diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItemTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItemTest.java new file mode 100644 index 00000000000..66b8572e86e --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItemTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.head; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.apache.wicket.mock.MockWebResponse; +import org.junit.jupiter.api.Test; + +class JavaScriptContentHeaderItemTest +{ + @Test + void outputsTextJavascriptAsTypeIfNoTypeSet() + { + JavaScriptContentHeaderItem item = new JavaScriptContentHeaderItem("", "the id"); + MockWebResponse response = new MockWebResponse(); + + item.render(response); + + assertEquals(""" + + """, response.getTextResponse().toString()); + } + + @Test + void outputsTypeThatIsSet() + { + JavaScriptContentHeaderItem item = new JavaScriptContentHeaderItem("", "the id") + .setType(JavaScriptBrowserProcessedContentType.MODULE); + MockWebResponse response = new MockWebResponse(); + + item.render(response); + + assertEquals(""" + + """, response.getTextResponse().toString()); + } + + @Test + void itemsWithSameJavascriptAndDifferentTypesAreInequal() + { + JavaScriptContentHeaderItem item1 = new JavaScriptContentHeaderItem("", "the id") + .setType(JavaScriptBrowserProcessedContentType.TEXT_JAVASCRIPT); + JavaScriptContentHeaderItem item2 = new JavaScriptContentHeaderItem("", "the id") + .setType(JavaScriptBrowserProcessedContentType.MODULE); + + assertNotEquals(item1, item2); + } + + @Test + void itemsWithSameJavascriptAndDifferentTypesHaveDifferentHashCodes() + { + JavaScriptContentHeaderItem item1 = new JavaScriptContentHeaderItem("", "the id") + .setType(JavaScriptBrowserProcessedContentType.TEXT_JAVASCRIPT); + JavaScriptContentHeaderItem item2 = new JavaScriptContentHeaderItem("", "the id") + .setType(JavaScriptBrowserProcessedContentType.MODULE); + + assertNotEquals(item1.hashCode(), item2.hashCode()); + } +} diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptDataBlockTypeTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptDataBlockTypeTest.java new file mode 100644 index 00000000000..3a938aa0661 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/head/JavaScriptDataBlockTypeTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.head; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class JavaScriptDataBlockTypeTest +{ + @Test + void cannotBeCreatedWithNullType() + { + String message = assertThrows(IllegalArgumentException.class, + () -> new JavaScriptDataBlockType(null)) + .getMessage(); + assertEquals("Argument 'type' may not be null.", message); + } + + @Test + void cannotBeCreatedWithTypeThatIsNotAMediaType() + { + String message = assertThrows(IllegalArgumentException.class, + () -> new JavaScriptDataBlockType("Not a media type")) + .getMessage(); + assertEquals("'type' must be a media type (that is: start with a type followed by a slash).", message); + } + + @Test + void cannotBeCreatedWithTypeThatIsAJavaScriptMediaType() + { + String message = assertThrows(IllegalArgumentException.class, + () -> new JavaScriptDataBlockType("text/javascript")) + .getMessage(); + assertEquals("'type' may not be a JavaScript media type.", message); + } + + @Test + void creationWithTypeThatIsNotAJavaScriptMediaTypeSucceeds() + { + assertDoesNotThrow(() -> new JavaScriptDataBlockType("application/json")); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptBrowserProcessedContentType.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptBrowserProcessedContentType.java new file mode 100644 index 00000000000..8beb46c4c87 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptBrowserProcessedContentType.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.head; + +/** + * A browser- + * processed type for {@link JavaScriptContentHeaderItem}. That is, the content is not a data block. + */ +public enum JavaScriptBrowserProcessedContentType implements JavaScriptContentType +{ + IMPORT_MAP("importmap"), + MODULE("module"), + SPECULATION_RULES("speculationrules"), + TEXT_JAVASCRIPT("text/javascript"); + + private final String type; + + JavaScriptBrowserProcessedContentType(String type) + { + this.type = type; + } + + @Override + public String getType() + { + return type; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java index 0c8839d6f47..82cfea82360 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentHeaderItem.java @@ -26,7 +26,7 @@ import org.apache.wicket.util.value.AttributeMap; /** - * {@link HeaderItem} for internal (embedded in the header) javascript content. + * {@link HeaderItem} for internal (embedded in the header) JavaScript content. * * @author papegaaij */ @@ -34,13 +34,15 @@ public class JavaScriptContentHeaderItem extends JavaScriptHeaderItem { private final CharSequence javaScript; + private JavaScriptContentType type = JavaScriptBrowserProcessedContentType.TEXT_JAVASCRIPT; + /** * Creates a new {@code JavaScriptContentHeaderItem}. * * @param javaScript - * javascript content to be rendered. + * JavaScript content to be rendered. * @param id - * unique id for the javascript element. This can be null, however in that case the + * unique id for the JavaScript element. This can be null, however in that case the * ajax header contribution can't detect duplicate script fragments. */ public JavaScriptContentHeaderItem(CharSequence javaScript, String id) @@ -50,18 +52,39 @@ public JavaScriptContentHeaderItem(CharSequence javaScript, String id) } /** - * @return javascript content to be rendered. + * @return JavaScript content to be rendered. */ public CharSequence getJavaScript() { return javaScript; } + public JavaScriptContentType getType() + { + return type; + } + + /** + * Set the type of + * the script. If no type is set, it defaults to {@link JavaScriptReferenceType#TEXT_JAVASCRIPT}. + * + * @param type the new type. + */ + public JavaScriptContentHeaderItem setType(final JavaScriptContentType type) + { + this.type = type; + return this; + } + @Override public void render(Response response) { AttributeMap attributes = new AttributeMap(); - attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript"); + // No attribute or an empty string works the same as `text/javascript`, + // but use the latter for backward compatibility. + JavaScriptContentType actualType = type == null ? + JavaScriptBrowserProcessedContentType.TEXT_JAVASCRIPT : type; + attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, actualType.getType()); attributes.putAttribute(JavaScriptUtils.ATTR_ID, getId()); attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce()); JavaScriptUtils.writeInlineScript(response, getJavaScript(), attributes); @@ -88,7 +111,8 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; JavaScriptContentHeaderItem that = (JavaScriptContentHeaderItem) o; - return Objects.equals(javaScript, that.javaScript); + return Objects.equals(javaScript, that.javaScript) && + Objects.equals(type, that.type); } @Override @@ -97,6 +121,7 @@ public int hashCode() // Not using `Objects.hash` for performance reasons int result = super.hashCode(); result = 31 * result + ((javaScript != null) ? javaScript.hashCode() : 0); + result = 31 * result + ((type != null) ? type.hashCode() : 0); return result; } } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentType.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentType.java new file mode 100644 index 00000000000..5cab1d558d8 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptContentType.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.head; + +import org.apache.wicket.util.io.IClusterable; + +/** + * To be used to define the "type" attribute of the script tag written + * by a {@link JavaScriptContentHeaderItem}. + */ +public interface JavaScriptContentType extends IClusterable +{ + String getType(); +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptDataBlockType.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptDataBlockType.java new file mode 100644 index 00000000000..4fe5abb0053 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptDataBlockType.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.head; + +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.wicket.util.lang.Args; + +/** + * A data block + * type (that is, any other value than a browser-processed value) for {@link JavaScriptContentHeaderItem}. + */ +public class JavaScriptDataBlockType implements JavaScriptContentType +{ + private static final Pattern TYPE_PREFIX_PATTERN = Pattern.compile("^[-!#$%&'*+.0-9A-Z^_`a-z{|}~]+/"); + private static final Set JAVASCRIPT_MEDIA_TYPES = Set.of( + "text/javascript", + "application/javascript", + "application/ecmascript", + "application/x-ecmascript", + "application/x-javascript", + "text/ecmascript", + "text/javascript1.0", + "text/javascript1.1", + "text/javascript1.2", + "text/javascript1.3", + "text/javascript1.4", + "text/javascript1.5", + "text/jscript", + "text/livescript", + "text/x-ecmascript", + "text/x-javascript" + ); + + private final String type; + + /** + * Create a new data block type with the given media type. + *

+ * A rudimentary check is done to ensure type is a media type. It must start with a + * token followed by a slash. No check is done for + * what follows the slash. + *

+ * The type may also not be one of the JavaScript media types defined in + * text/javacript + * on MDN. + * + * @param type the media type of the data block. + */ + public JavaScriptDataBlockType(String type) + { + Args.notNull(type, "type"); + Args.isTrue(TYPE_PREFIX_PATTERN.matcher(type).find(), + "'type' must be a media type (that is: start with a type followed by a slash)."); + Args.isFalse(JAVASCRIPT_MEDIA_TYPES.contains(type.toLowerCase(Locale.ENGLISH)), + "'type' may not be a JavaScript media type."); + + this.type = type; + } + + @Override + public String getType() + { + return type; + } +}