You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2019/08/05 20:10:42 UTC
[wicket] branch master updated: WICKET-6682 add CSP nonce support
This is an automated email from the ASF dual-hosted git repository.
svenmeier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/master by this push:
new 4027b54 WICKET-6682 add CSP nonce support
4027b54 is described below
commit 4027b5456031dae79af7ca0a14a1fe59719653ad
Author: Andrew Kondratev <an...@gmail.com>
AuthorDate: Tue Jul 23 17:43:42 2019 +0200
WICKET-6682 add CSP nonce support
reverts WICKET-4777 since all markup attributes have to be encoded
this closes #376
---
.../apache/wicket/core/util/string/CssUtils.java | 100 +++++++++++-----
.../wicket/core/util/string/JavaScriptUtils.java | 129 +++++++++++++++------
.../wicket/markup/head/AbstractCspHeaderItem.java | 102 ++++++++--------
.../wicket/markup/head/CssContentHeaderItem.java | 6 +-
.../apache/wicket/markup/head/CssHeaderItem.java | 15 ++-
.../markup/head/JavaScriptContentHeaderItem.java | 9 +-
.../wicket/markup/head/JavaScriptHeaderItem.java | 22 ++--
.../markup/head/JavaScriptReferenceHeaderItem.java | 2 -
.../wicket/markup/head/MetaDataHeaderItem.java | 35 ++++++
.../wicket/markup/head/OnDomReadyHeaderItem.java | 10 +-
.../wicket/markup/head/OnEventHeaderItem.java | 8 +-
.../wicket/markup/head/OnLoadHeaderItem.java | 10 +-
.../markup/head/filter/CspNonceHeaderResponse.java | 93 +++++++++++++++
.../wicket/markup/html/pages/BrowserInfoPage.java | 2 +-
.../org/apache/wicket/markup/parser/XmlTag.java | 64 +++-------
.../wicket/core/util/string/CssUtilsTest.java | 17 ++-
.../core/util/string/JavaScriptUtilsTest.java | 47 ++++----
.../wicket/markup/head/filter/CspNoncePage.html | 4 +
.../wicket/markup/head/filter/CspNoncePage.java | 96 ++++++++-------
.../markup/head/filter/CspNoncePageExpected.html | 25 ++++
.../head/filter/FilteringHeaderResponseTest.java | 11 ++
.../wicket/page/XmlPartialPageUpdateTest.java | 2 +-
.../apache/wicket/examples/csp/CspApplication.java | 72 ++++++++++++
.../apache/wicket/examples/csp/NonceDemoPage.html | 34 ++++++
.../apache/wicket/examples/csp/NonceDemoPage.java | 81 +++++++++++++
.../wicket/examples/csp/NonceDemoPage.properties | 21 ++++
.../apache/wicket/examples/homepage/HomePage.html | 1 +
wicket-examples/src/main/webapp/WEB-INF/web.xml | 16 +++
.../org/apache/wicket/util/value/AttributeMap.java | 87 +++++++++++++-
.../org/apache/wicket/util/value/ValueMap.java | 3 +-
.../apache/wicket/util/value/AttributeMapTest.java | 69 +++++++++++
.../org/apache/wicket/util/value/ValueMapTest.java | 3 +
32 files changed, 937 insertions(+), 259 deletions(-)
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
index b028b43..521decf 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/CssUtils.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
import org.apache.wicket.request.Response;
import org.apache.wicket.util.lang.Classes;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* Utility methods for CSS.
@@ -27,6 +28,7 @@ import org.apache.wicket.util.string.Strings;
*/
public final class CssUtils
{
+ // FIXME type text/css can be omitted for the style tag in supported browsers
/** start of CSS inline open tag */
public final static String INLINE_OPEN_TAG_START = "<style type=\"text/css\"";
@@ -36,6 +38,13 @@ public final class CssUtils
/** CSS inline close tag */
public final static String INLINE_CLOSE_TAG = "</style>\n";
+ public static final String ATTR_ID = "id";
+ public static final String ATTR_TYPE = "type";
+ public static final String ATTR_LINK_HREF = "href";
+ public static final String ATTR_LINK_MEDIA = "media";
+ public static final String ATTR_LINK_REL = "rel";
+ public static final String ATTR_CSP_NONCE = "nonce";
+
/**
* Hidden constructor.
*/
@@ -52,7 +61,9 @@ public final class CssUtils
* The text to added in between the style tags
* @param id
* Unique identifier of element
+ * @deprecated please use {@link #writeInlineStyle(Response, CharSequence, AttributeMap)} instead
*/
+ @Deprecated
public static void writeCss(final Response response, final CharSequence text, String id)
{
writeOpenTag(response, id);
@@ -61,17 +72,49 @@ public final class CssUtils
}
/**
+ * Write the simple text to the response object surrounded by a style tag.
+ * In most cases the text simply an inline CSS.
+ *
+ * @param response
+ * The HTTP: response
+ * @param text
+ * The text to added in between the style tags
+ * @param attributes
+ * Tag attributes map
+ * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style">Style HTML Element</a>
+ */
+ public static void writeInlineStyle(final Response response, final CharSequence text, AttributeMap attributes)
+ {
+ writeOpenTag(response, attributes);
+ response.write(text);
+ writeCloseTag(response);
+ }
+
+ /**
+ * Write open style tag for the inline CSS
*
* @param response
* @param id
*/
public static void writeOpenTag(final Response response, String id)
{
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(ATTR_ID, id);
+ writeOpenTag(response, attributes);
+ }
+
+ /**
+ * Write open style tag for the inline CSS
+ *
+ * @param response
+ * the response to write to
+ * @param attributes
+ * Tag attributes map
+ */
+ public static void writeOpenTag(final Response response, AttributeMap attributes)
+ {
response.write(INLINE_OPEN_TAG_START);
- if (id != null)
- {
- response.write(" id=\"" + id + "\"");
- }
+ response.write(attributes.toCharSequence());
response.write(">\n");
}
@@ -95,7 +138,9 @@ public final class CssUtils
* the CSS media
* @param markupId
* the markupId
+ * @deprecated please use {@link #writeLink(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeLinkUrl(final Response response, final CharSequence url,
final CharSequence media, final String markupId)
{
@@ -115,31 +160,34 @@ public final class CssUtils
* the markupId
* @param rel
* the rel attribute
+ * @deprecated please use {@link #writeLink(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeLinkUrl(final Response response, final CharSequence url,
final CharSequence media, final String markupId, final String rel)
{
- response.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"");
- response.write(Strings.escapeMarkup(url));
- response.write("\"");
- if (Strings.isEmpty(media) == false)
- {
- response.write(" media=\"");
- response.write(Strings.escapeMarkup(media));
- response.write("\"");
- }
- if (Strings.isEmpty(markupId) == false)
- {
- response.write(" id=\"");
- response.write(Strings.escapeMarkup(markupId));
- response.write("\"");
- }
- if (Strings.isEmpty(rel) == false)
- {
- response.write(" rel=\"");
- response.write(Strings.escapeMarkup(rel));
- response.write("\"");
- }
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(ATTR_LINK_REL, Strings.isEmpty(rel) ? "stylesheet" : rel);
+ attributes.putAttribute(ATTR_TYPE, "text/css");
+ attributes.putAttribute(ATTR_LINK_HREF, url);
+ attributes.putAttribute(ATTR_LINK_MEDIA, media.toString());
+ attributes.putAttribute(ATTR_ID, markupId);
+ writeLink(response, attributes);
+ }
+
+ /**
+ * Writes a reference to a css file in the response object
+ *
+ * @param response
+ * the response to write to
+ * @param attributes
+ * Attributes map
+ * @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link">Link HTML Element</a>
+ */
+ public static void writeLink(final Response response, AttributeMap attributes)
+ {
+ response.write("<link");
+ response.write(attributes.toCharSequence());
response.write(" />");
}
@@ -156,4 +204,4 @@ public final class CssUtils
{
return Classes.simpleName(scope) + ".CSS." + facet;
}
-}
+}
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
index 8e4d042..08b6a22 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java
@@ -18,7 +18,7 @@ package org.apache.wicket.core.util.string;
import org.apache.wicket.request.Response;
import org.apache.wicket.util.string.Strings;
-
+import org.apache.wicket.util.value.AttributeMap;
/**
* Provide some helpers to write javascript related tags to the response object.
@@ -46,6 +46,13 @@ public class JavaScriptUtils
/** Script close tag including content suffix */
public final static String SCRIPT_CLOSE_TAG = SCRIPT_CONTENT_SUFFIX + "</script>\n";
+ public static final String ATTR_ID = "id";
+ public static final String ATTR_TYPE = "type";
+ public static final String ATTR_SCRIPT_SRC = "src";
+ public static final String ATTR_SCRIPT_DEFER = "defer";
+ public static final String ATTR_SCRIPT_ASYNC = "async";
+ public static final String ATTR_CSP_NONCE = "nonce";
+
/** The response object */
private final Response response;
@@ -104,7 +111,9 @@ public class JavaScriptUtils
* The javascript file URL
* @param id
* Unique identifier of element
+ * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeJavaScriptUrl(final Response response, final CharSequence url,
final String id)
{
@@ -125,7 +134,9 @@ public class JavaScriptUtils
* the page has been loaded.
* @param charset
* a non null value specifies the charset attribute of the script tag
+ * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeJavaScriptUrl(final Response response, final CharSequence url,
final String id, boolean defer, String charset)
{
@@ -148,32 +159,38 @@ public class JavaScriptUtils
* a non null value specifies the charset attribute of the script tag
* @param async
* specifies that the script can be loaded asynchronously by the browser
+ * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeJavaScriptUrl(final Response response, final CharSequence url,
final String id, boolean defer, String charset, boolean async)
{
- response.write("<script type=\"text/javascript\" ");
- if (id != null)
- {
- response.write("id=\"" + Strings.escapeMarkup(id) + "\" ");
- }
- if (defer)
- {
- response.write("defer=\"defer\" ");
- }
-
- if (async)
- {
- response.write("async=\"async\" ");
- }
+ AttributeMap attributes = new AttributeMap();
+ // XXX JS mimetype can be omitted (also see below)
+ attributes.putAttribute(ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(ATTR_SCRIPT_SRC, url);
+ attributes.putAttribute(ATTR_ID, id);
+ attributes.putAttribute(ATTR_SCRIPT_DEFER, defer);
+ attributes.putAttribute(ATTR_SCRIPT_ASYNC, async);
+ // FIXME charset attr is deprecated
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Deprecated_attributes
+ attributes.putAttribute("charset", charset);
+ writeScript(response, attributes);
+ }
- if (charset != null)
- {
- response.write("charset=\"" + Strings.escapeMarkup(charset) + "\" ");
- }
- response.write("src=\"");
- response.write(url);
- response.write("\"></script>");
+ /**
+ * Write a reference to a javascript file to the response object
+ *
+ * @param response
+ * The HTTP response
+ * @param attributes
+ * Extra tag attributes
+ */
+ public static void writeScript(final Response response, AttributeMap attributes)
+ {
+ response.write("<script");
+ response.write(attributes.toCharSequence());
+ response.write("></script>");
response.write("\n");
}
@@ -184,7 +201,9 @@ public class JavaScriptUtils
* The HTTP response
* @param url
* The javascript file URL
+ * @deprecated please use {@link #writeScript(Response, AttributeMap)} instead
*/
+ @Deprecated
public static void writeJavaScriptUrl(final Response response, final CharSequence url)
{
writeJavaScriptUrl(response, url, null);
@@ -199,6 +218,7 @@ public class JavaScriptUtils
* The text to added in between the script tags
* @param id
* Unique identifier of element
+ * @deprecated please use {@link #writeInlineScript(Response, CharSequence, AttributeMap)} instead
*/
public static void writeJavaScript(final Response response, final CharSequence text, String id)
{
@@ -209,6 +229,23 @@ public class JavaScriptUtils
/**
* Write the simple text to the response object surrounded by a script tag.
+ *
+ * @param response
+ * The HTTP: response
+ * @param text
+ * The text to added in between the script tags
+ * @param attributes
+ * Extra tag attributes
+ */
+ public static void writeInlineScript(final Response response, final CharSequence text, AttributeMap attributes)
+ {
+ writeOpenTag(response, attributes);
+ response.write(Strings.replaceAll(text, "</", "<\\/"));
+ writeCloseTag(response);
+ }
+
+ /**
+ * Write the simple text to the response object surrounded by a script tag.
*
* @param response
* The HTTP: response
@@ -217,21 +254,39 @@ public class JavaScriptUtils
*/
public static void writeJavaScript(final Response response, final CharSequence text)
{
- writeJavaScript(response, text, null);
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(ATTR_TYPE, "text/javascript");
+ writeInlineScript(response, text, attributes);
}
/**
*
* @param response
* @param id
+ * @deprecated please use {@link #writeOpenTag(Response, AttributeMap)}
*/
+ @Deprecated
public static void writeOpenTag(final Response response, String id)
{
- response.write("<script type=\"text/javascript\" ");
- if (id != null)
- {
- response.write("id=\"" + Strings.escapeMarkup(id) + "\"");
- }
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(ATTR_ID, id);
+ writeOpenTag(response, attributes);
+ }
+
+ /**
+ * Write open script tag for inline script.
+ * Content is prefixed with {@link #SCRIPT_CONTENT_PREFIX}.
+ *
+ * @param response
+ * the response to write to
+ * @param attributes
+ * Tag attributes map
+ */
+ public static void writeOpenTag(final Response response, AttributeMap attributes)
+ {
+ response.write("<script");
+ response.write(attributes.toCharSequence());
response.write(">");
response.write(SCRIPT_CONTENT_PREFIX);
}
@@ -239,15 +294,21 @@ public class JavaScriptUtils
/**
*
* @param response
+ * @deprecated please use {@link #writeOpenTag(Response, AttributeMap)}
*/
+ @Deprecated
public static void writeOpenTag(final Response response)
{
- writeOpenTag(response, null);
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(ATTR_TYPE, "text/javascript");
+ writeOpenTag(response, attributes);
}
/**
- *
+ * Write close script tag for inline script. The close tag is prefixed with {@link #SCRIPT_CONTENT_SUFFIX}
+ *
* @param response
+ * the response to write to
*/
public static void writeCloseTag(final Response response)
{
@@ -274,10 +335,12 @@ public class JavaScriptUtils
}
/**
- * Write the script close tag to the response. The response output stream remains open.
+ * Write the inline script close tag to the response. The response output stream remains open.
+ * Calls {@link #writeCloseTag(Response)} internally.
+ * The close tag is prefixed with {@link #SCRIPT_CONTENT_SUFFIX}.
*/
public void close()
{
writeCloseTag(response);
- }
-}
+ }
+}
\ No newline at end of file
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
old mode 100755
new mode 100644
similarity index 61%
copy from wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
copy to wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
index 8a6f297..d754c86
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/AbstractCspHeaderItem.java
@@ -1,49 +1,53 @@
-/*
- * 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.util.value;
-
-import java.util.Map;
-
-/**
- * <code>ValueMap</code> for attributes.
- *
- * @author Eelco Hillenius
- * @since 1.2.6
- */
-public final class AttributeMap extends ValueMap
-{
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs an empty <code>AttributeMap</code>.
- */
- public AttributeMap()
- {
- super();
- }
-
- /**
- * Copy constructor.
- *
- * @param map
- * a <code>Map</code> to be copied
- */
- public AttributeMap(final Map<String, Object> map)
- {
- super(map);
- }
-}
+/*
+ * 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;
+
+/**
+ * Abstract base class for all {@link HeaderItem}s that have a <em>nonce</em> to
+ * allow the browser to check <em>Content Security Policye</em> (CSP) .
+ *
+ * @author svenmeier
+ */
+public abstract class AbstractCspHeaderItem extends HeaderItem
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * CSP Nonce
+ */
+ private String nonce;
+
+ /**
+ * @return CSP nonce
+ */
+ public String getNonce()
+ {
+ return nonce;
+ }
+
+ /**
+ * Set the CSP nonce
+ *
+ * @param nonce
+ * @return {@code this} object, for method chaining
+ */
+ public AbstractCspHeaderItem setNonce(String nonce)
+ {
+ this.nonce = nonce;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
index dee0a63..97d14bd 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssContentHeaderItem.java
@@ -23,6 +23,7 @@ import java.util.Objects;
import org.apache.wicket.core.util.string.CssUtils;
import org.apache.wicket.request.Response;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* {@link HeaderItem} for internal (embedded in the header) css content.
@@ -72,7 +73,10 @@ public class CssContentHeaderItem extends CssHeaderItem
response.write("]>");
}
- CssUtils.writeCss(response, getCss(), getId());
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(CssUtils.ATTR_ID, getId());
+ attributes.putAttribute(CssUtils.ATTR_CSP_NONCE, getNonce());
+ CssUtils.writeInlineStyle(response, getCss(), attributes);
if (hasCondition)
{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
index c269d8f..8ac04a3 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/CssHeaderItem.java
@@ -26,6 +26,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +36,7 @@ import org.slf4j.LoggerFactory;
*
* @author papegaaij
*/
-public abstract class CssHeaderItem extends HeaderItem
+public abstract class CssHeaderItem extends AbstractCspHeaderItem
{
private static final long serialVersionUID = 1L;
@@ -61,7 +62,7 @@ public abstract class CssHeaderItem extends HeaderItem
private final String condition;
private String markupId;
-
+
protected CssHeaderItem(String condition)
{
this.condition = condition;
@@ -355,8 +356,14 @@ public abstract class CssHeaderItem extends HeaderItem
response.write(condition);
response.write("]>");
}
-
- CssUtils.writeLinkUrl(response, url, media, getId(), rel);
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(CssUtils.ATTR_LINK_REL, rel == null ? "stylesheet" : rel);
+ attributes.putAttribute(CssUtils.ATTR_TYPE, "text/css");
+ attributes.putAttribute(CssUtils.ATTR_LINK_HREF, url);
+ attributes.putAttribute(CssUtils.ATTR_ID, getId());
+ attributes.putAttribute(CssUtils.ATTR_LINK_MEDIA, media);
+ attributes.putAttribute(CssUtils.ATTR_CSP_NONCE, getNonce());
+ CssUtils.writeLink(response, attributes);
if (hasCondition)
{
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 2b3f8d5..fccadee 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
@@ -20,9 +20,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
-import org.apache.wicket.request.Response;
import org.apache.wicket.core.util.string.JavaScriptUtils;
+import org.apache.wicket.request.Response;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* {@link HeaderItem} for internal (embedded in the header) javascript content.
@@ -69,7 +70,11 @@ public class JavaScriptContentHeaderItem extends JavaScriptHeaderItem
response.write(getCondition());
response.write("]>");
}
- JavaScriptUtils.writeJavaScript(response, getJavaScript(), getId());
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_ID, getId());
+ attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+ JavaScriptUtils.writeInlineScript(response, getJavaScript(), attributes);
if (hasCondition)
{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
index 0b43443..e72f834 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptHeaderItem.java
@@ -18,14 +18,13 @@ package org.apache.wicket.markup.head;
import java.util.Objects;
-import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.core.util.string.JavaScriptUtils;
import org.apache.wicket.request.Response;
-import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* Base class for all {@link HeaderItem}s that represent javascripts. This class mainly contains
@@ -33,8 +32,10 @@ import org.apache.wicket.util.string.Strings;
*
* @author papegaaij
*/
-public abstract class JavaScriptHeaderItem extends HeaderItem
+public abstract class JavaScriptHeaderItem extends AbstractCspHeaderItem
{
+ private static final long serialVersionUID = 1L;
+
/**
* The condition to use for Internet Explorer conditional comments. E.g. "IE 7".
* {@code null} or empty string for no condition.
@@ -362,11 +363,16 @@ public abstract class JavaScriptHeaderItem extends HeaderItem
response.write("]>");
}
- boolean isAjax = RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent();
- // the url needs to be escaped when Ajax, because it will break the Ajax Response XML (WICKET-4777)
- CharSequence escapedUrl = isAjax ? Strings.escapeMarkup(url): url;
-
- JavaScriptUtils.writeJavaScriptUrl(response, escapedUrl, id, defer, charset, async);
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_ID, id);
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, defer);
+ // XXX this attribute is not necessary for modern browsers
+ attributes.putAttribute("charset", charset);
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_ASYNC, async);
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, url);
+ attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+ JavaScriptUtils.writeScript(response, attributes);
if (hasCondition)
{
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
index ca52cfb..c5209cc 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/JavaScriptReferenceHeaderItem.java
@@ -20,7 +20,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
@@ -29,7 +28,6 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.resource.bundles.IResourceBundle;
import org.apache.wicket.util.lang.Args;
-import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.Strings;
/**
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
index 77fce4b..38efb5b 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/MetaDataHeaderItem.java
@@ -162,6 +162,41 @@ public class MetaDataHeaderItem extends HeaderItem
/**
* Factory method to create <meta> tag.
*
+ * @param httpEquiv
+ * the 'httpEquiv' attribute of the tag
+ * @param content
+ * the 'content' attribute of the tag
+ * @return
+ * A new {@link MetaDataHeaderItem}
+ */
+ public static MetaDataHeaderItem forHttpEquiv(String httpEquiv, String content)
+ {
+ return forHttpEquiv(Model.of(httpEquiv), Model.of(content));
+ }
+
+ /**
+ * Factory method to create <meta> tag.
+ *
+ * @param httpEquiv
+ * the 'httpEquiv' attribute of the tag
+ * @param content
+ * the 'content' attribute of the tag
+ * @return
+ * A new {@link MetaDataHeaderItem}
+ */
+ public static MetaDataHeaderItem forHttpEquiv(IModel<String> httpEquiv, IModel<String> content)
+ {
+ MetaDataHeaderItem headerItem = new MetaDataHeaderItem(META_TAG);
+
+ headerItem.addTagAttribute("http-equiv", httpEquiv);
+ headerItem.addTagAttribute("content", content);
+
+ return headerItem;
+ }
+
+ /**
+ * Factory method to create <meta> tag.
+ *
* @param name
* the 'name' attribute of the tag
* @param content
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
index cc51588..d3a829d 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnDomReadyHeaderItem.java
@@ -26,6 +26,7 @@ import org.apache.wicket.request.Response;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.settings.JavaScriptLibrarySettings;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* {@link HeaderItem} for scripts that need to be executed directly after the DOM has been built,
@@ -33,7 +34,7 @@ import org.apache.wicket.util.string.Strings;
*
* @author papegaaij
*/
-public class OnDomReadyHeaderItem extends HeaderItem
+public class OnDomReadyHeaderItem extends AbstractCspHeaderItem
{
private static final long serialVersionUID = 1L;
@@ -86,8 +87,11 @@ public class OnDomReadyHeaderItem extends HeaderItem
CharSequence js = getJavaScript();
if (Strings.isEmpty(js) == false)
{
- JavaScriptUtils.writeJavaScript(response, "Wicket.Event.add(window, \"domready\", " +
- "function(event) { " + js + ";});");
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+ JavaScriptUtils.writeInlineScript(response, "Wicket.Event.add(window, \"domready\", " +
+ "function(event) { " + js + ";});", attributes);
}
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
index fd1bf60..f3b7baf 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnEventHeaderItem.java
@@ -28,13 +28,14 @@ import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.settings.JavaScriptLibrarySettings;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* {@link HeaderItem} for event triggered scripts.
*
* @author papegaaij
*/
-public class OnEventHeaderItem extends HeaderItem
+public class OnEventHeaderItem extends AbstractCspHeaderItem
{
private static final long serialVersionUID = 1L;
@@ -118,7 +119,10 @@ public class OnEventHeaderItem extends HeaderItem
{
if (Strings.isEmpty(getJavaScript()) == false)
{
- JavaScriptUtils.writeJavaScript(response, getCompleteJavaScript());
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+ JavaScriptUtils.writeInlineScript(response, getCompleteJavaScript(), attributes);
}
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
index 3cd2cde..1ceb4fc 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/OnLoadHeaderItem.java
@@ -26,13 +26,14 @@ import org.apache.wicket.request.Response;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.settings.JavaScriptLibrarySettings;
import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
/**
* {@link HeaderItem} for scripts that need to be executed after the entire page is loaded.
*
* @author papegaaij
*/
-public class OnLoadHeaderItem extends HeaderItem
+public class OnLoadHeaderItem extends AbstractCspHeaderItem
{
private static final long serialVersionUID = 1L;
@@ -86,8 +87,11 @@ public class OnLoadHeaderItem extends HeaderItem
CharSequence js = getJavaScript();
if (Strings.isEmpty(js) == false)
{
- JavaScriptUtils.writeJavaScript(response, "Wicket.Event.add(window, \"load\", " +
- "function(event) { " + js + ";});");
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_CSP_NONCE, getNonce());
+ JavaScriptUtils.writeInlineScript(response, "Wicket.Event.add(window, \"load\", " +
+ "function(event) { " + js + ";});", attributes);
}
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java b/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java
new file mode 100644
index 0000000..7a911ca
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java
@@ -0,0 +1,93 @@
+/*
+ * 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.filter;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.markup.head.AbstractCspHeaderItem;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.IWrappedHeaderItem;
+import org.apache.wicket.markup.head.MetaDataHeaderItem;
+import org.apache.wicket.markup.head.ResourceAggregator;
+import org.apache.wicket.markup.html.DecoratingHeaderResponse;
+
+/**
+ * Add a <em>Content Security Policy<em> (CSP) nonce to all relevant JavaScript and CSS header items.
+ * <p>
+ * Note: please don't forget to wrap with {@link ResourceAggregator} when setting it up with
+ * {@link Application#setHeaderResponseDecorator}, otherwise dependencies will not be rendered.
+ *
+ * @see AbstractCspHeaderItem
+ */
+public class CspNonceHeaderResponse extends DecoratingHeaderResponse
+{
+ private static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
+
+ /**
+ * Has the <em>Content-Security-Policy</em> header been rendered once.
+ */
+ private boolean policyRendered = false;
+
+ private String nonce;
+
+ public CspNonceHeaderResponse(IHeaderResponse real, String nonce)
+ {
+ super(real);
+
+ this.nonce = nonce;
+ }
+
+ @Override
+ public void render(HeaderItem item)
+ {
+ while (item instanceof IWrappedHeaderItem)
+ {
+ item = ((IWrappedHeaderItem)item).getWrapped();
+ }
+
+ if (item instanceof AbstractCspHeaderItem)
+ {
+ if (policyRendered == false)
+ {
+ policyRendered = true;
+
+ String policy = getContentSecurityPolicy(nonce);
+
+ super.render(MetaDataHeaderItem.forHttpEquiv(CONTENT_SECURITY_POLICY, policy));
+ }
+
+ ((AbstractCspHeaderItem)item).setNonce(nonce);
+ }
+
+ super.render(item);
+ }
+
+ /**
+ * Get the <em>Content-Security-Policy</em> (CSP).
+ * <p>
+ * There is a variety of CSP configurations, this default implementation uses the nonce for scripts and styles
+ * and allows <code>unsafe-eval</code>s (needed for Wicket Ajax).
+ *
+ * @param nonce
+ * the nonce
+ * @return content security policy
+ */
+ protected String getContentSecurityPolicy(String nonce)
+ {
+ return String.format("script-src 'unsafe-eval' 'nonce-%1$s'; style-src 'nonce-%1$s';", nonce);
+ }
+}
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
index 8d2cdc8..0541fa0 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/pages/BrowserInfoPage.java
@@ -139,7 +139,7 @@ public class BrowserInfoPage extends WebPage
{
String content = "0; url=" + getURL();
- response.render(new MetaDataHeaderItem(MetaDataHeaderItem.META_TAG).addTagAttribute("http-equiv", "refresh").addTagAttribute("content", content));
+ response.render(MetaDataHeaderItem.forHttpEquiv("refresh", content));
}
@Override
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java b/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
index 530bd29..eb4f276 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/parser/XmlTag.java
@@ -16,16 +16,13 @@
*/
package org.apache.wicket.markup.parser;
-import java.util.Iterator;
import java.util.Map;
-import org.apache.wicket.markup.parser.IXmlPullParser.HttpTagType;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.StringValue;
-import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.value.AttributeMap;
import org.apache.wicket.util.value.IValueMap;
-import org.apache.wicket.util.value.ValueMap;
/**
@@ -41,26 +38,19 @@ public class XmlTag
*/
public static enum TagType {
/** A close tag, like </TAG>. */
- CLOSE("CLOSE"),
+ CLOSE,
/** An open tag, like <TAG componentId = "xyz">. */
- OPEN("OPEN"),
+ OPEN,
/** An open/close tag, like <TAG componentId = "xyz"/>. */
- OPEN_CLOSE("OPEN_CLOSE");
-
- private String name;
-
- TagType(final String name)
- {
- this.name = name;
- }
+ OPEN_CLOSE;
}
TextSegment text;
/** Attribute map. */
- private IValueMap attributes;
+ private AttributeMap attributes;
/** Name of tag, such as "img" or "input". */
String name;
@@ -80,8 +70,6 @@ public class XmlTag
/** True if this tag is mutable, false otherwise. */
private boolean isMutable = true;
- private HttpTagType httpTagType;
-
/**
* Construct.
*/
@@ -139,15 +127,20 @@ public class XmlTag
*/
public IValueMap getAttributes()
{
+ return attributes();
+ }
+
+ private AttributeMap attributes()
+ {
if (attributes == null)
{
if ((copyOf == this) || (copyOf == null) || (copyOf.attributes == null))
{
- attributes = new ValueMap();
+ attributes = new AttributeMap();
}
else
{
- attributes = new ValueMap(copyOf.attributes);
+ attributes = new AttributeMap(copyOf.attributes);
}
}
return attributes;
@@ -349,7 +342,7 @@ public class XmlTag
dest.copyOf = copyOf;
if (attributes != null)
{
- dest.attributes = new ValueMap(attributes);
+ dest.attributes = new AttributeMap(attributes);
}
}
@@ -542,7 +535,7 @@ public class XmlTag
return text.text;
}
- return toXmlString(null);
+ return toXmlString();
}
/**
@@ -560,10 +553,9 @@ public class XmlTag
* Assuming some attributes have been changed, toXmlString() rebuilds the String on based on the
* tags informations.
*
- * @param attributeToBeIgnored
* @return A xml string matching the tag
*/
- public CharSequence toXmlString(final String attributeToBeIgnored)
+ public CharSequence toXmlString()
{
final AppendingStringBuffer buffer = new AppendingStringBuffer();
@@ -582,31 +574,7 @@ public class XmlTag
buffer.append(name);
- final IValueMap attributes = getAttributes();
- if (attributes.size() > 0)
- {
- final Iterator<String> iterator = attributes.keySet().iterator();
- for (; iterator.hasNext();)
- {
- final String key = iterator.next();
- if ((key != null) &&
- ((attributeToBeIgnored == null) || !key.equalsIgnoreCase(attributeToBeIgnored)))
- {
- buffer.append(' ');
- buffer.append(key);
- CharSequence value = getAttribute(key);
-
- // Attributes without values are possible, e.g. 'disabled'
- if (value != null)
- {
- buffer.append("=\"");
- value = Strings.escapeMarkup(value);
- buffer.append(value);
- buffer.append('"');
- }
- }
- }
- }
+ buffer.append(attributes().toCharSequence());
if (type == TagType.OPEN_CLOSE)
{
diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
index 4598d66..3217738 100644
--- a/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/CssUtilsTest.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.apache.wicket.response.StringResponse;
+import org.apache.wicket.util.value.AttributeMap;
import org.junit.jupiter.api.Test;
/**
@@ -32,13 +33,19 @@ class CssUtilsTest
* @throws Exception
*/
@Test
- void writeLinkUrl() throws Exception
+ void writeLink() throws Exception
{
StringResponse response = new StringResponse();
String url = "some/url;jsessionid=1234?with=parameters&p1=v1";
- String media = "some&bad&media";
- CssUtils.writeLinkUrl(response, url, media, "markupId");
-
- assertEquals("<link rel=\"stylesheet\" type=\"text/css\" href=\"some/url;jsessionid=1234?with=parameters&p1=v1\" media=\"some&bad&media\" id=\"markupId\" />", response.toString());
+ // Media may contain funny things, and it's OK
+ String media = "some&bad&media (min-height: 680px), screen and (orientation: portrait)";
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(CssUtils.ATTR_LINK_REL, "stylesheet");
+ attributes.putAttribute(CssUtils.ATTR_TYPE, "text/css");
+ attributes.putAttribute(CssUtils.ATTR_LINK_HREF, url);
+ attributes.putAttribute(CssUtils.ATTR_LINK_MEDIA, media);
+ attributes.putAttribute(CssUtils.ATTR_ID, "markupId");
+ CssUtils.writeLink(response, attributes);
+ assertEquals("<link rel=\"stylesheet\" type=\"text/css\" href=\"some/url;jsessionid=1234?with=parameters&p1=v1\" media=\"some&bad&media (min-height: 680px), screen and (orientation: portrait)\" id=\"markupId\" />", response.toString());
}
}
diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
index d89a036..b849101 100644
--- a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java
@@ -19,6 +19,7 @@ package org.apache.wicket.core.util.string;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.apache.wicket.response.StringResponse;
+import org.apache.wicket.util.value.AttributeMap;
import org.junit.jupiter.api.Test;
/**
@@ -32,17 +33,19 @@ class JavaScriptUtilsTest
* @throws Exception
*/
@Test
- void writeJavaScriptUrl() throws Exception
+ public void writeJavaScript() throws Exception
{
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_ID, "some\"funny<id&%");
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, true);
+ attributes.putAttribute("charset", "some\"funny<charset&%");
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, "some/url;jsessionid=1234?p1=v1&p2=v2");
StringResponse response = new StringResponse();
- String url = "some/url;jsessionid=1234?p1=v1&p2=v2";
- String id = "some&bad%id";
- boolean defer = true;
- String charset = "some&bad%%charset";
- JavaScriptUtils.writeJavaScriptUrl(response, url, id, defer, charset);
+ JavaScriptUtils.writeScript(response, attributes);
assertEquals(
- "<script type=\"text/javascript\" id=\"some&bad%id\" defer=\"defer\" charset=\"some&bad%%charset\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2\"></script>\n",
+ "<script type=\"text/javascript\" id=\"some"funny<id&%\" defer=\"defer\" charset=\"some"funny<charset&%\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2\"></script>\n",
response.toString());
}
@@ -50,31 +53,35 @@ class JavaScriptUtilsTest
* https://issues.apache.org/jira/browse/WICKET-5715
*/
@Test
- void writeJavaScriptUrlAsync()
+ public void writeJavaScriptAsync()
{
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ attributes.putAttribute(JavaScriptUtils.ATTR_ID, "some\"funny<id&%");
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_DEFER, true);
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_ASYNC, true);
+ attributes.putAttribute("charset", "some\"funny<charset&%");
+ attributes.putAttribute(JavaScriptUtils.ATTR_SCRIPT_SRC, "some/url;jsessionid=1234?p1=v1&p2=v2&p3=v3");
StringResponse response = new StringResponse();
- String url = "some/url;jsessionid=1234?p1=v1&p2=v2";
- String id = "some&bad%id";
- boolean defer = true;
- boolean async = true;
- String charset = "some&bad%%charset";
- JavaScriptUtils.writeJavaScriptUrl(response, url, id, defer, charset, async);
+ JavaScriptUtils.writeScript(response, attributes);
assertEquals(
- "<script type=\"text/javascript\" id=\"some&bad%id\" defer=\"defer\" async=\"async\" charset=\"some&bad%%charset\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2\"></script>\n",
+ "<script type=\"text/javascript\" id=\"some"funny<id&%\" defer=\"defer\" async=\"async\" charset=\"some"funny<charset&%\" src=\"some/url;jsessionid=1234?p1=v1&p2=v2&p3=v3\"></script>\n",
response.toString());
}
/**
*/
@Test
- void writeJavaScript()
+ public void writeInlineScript()
{
StringResponse response = new StringResponse();
- JavaScriptUtils.writeJavaScript(response,
- "var message = 'Scripts are written to the <script></script> tag'");
+ AttributeMap attributes = new AttributeMap();
+ attributes.putAttribute(JavaScriptUtils.ATTR_TYPE, "text/javascript");
+ JavaScriptUtils.writeInlineScript(response,
+ "var message = 'Scripts are written to the <script></script> tag'", attributes);
- assertEquals("<script type=\"text/javascript\" >\n" //
+ assertEquals("<script type=\"text/javascript\">\n" //
+ "/*<![CDATA[*/\n" //
+ "var message = 'Scripts are written to the <script><\\/script> tag'\n" //
+ "/*]]>*/\n"//
@@ -84,7 +91,7 @@ class JavaScriptUtilsTest
/**
*/
@Test
- void scriptTag()
+ public void scriptTag()
{
assertEquals("<script type=\"text/javascript\">\n/*<![CDATA[*/\n",
JavaScriptUtils.SCRIPT_OPEN_TAG);
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html
new file mode 100644
index 0000000..ee4a485
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.html
@@ -0,0 +1,4 @@
+<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
+ <body>
+ </body>
+</html>
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
old mode 100755
new mode 100644
similarity index 64%
copy from wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
copy to wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
index 8a6f297..2981f2e
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePage.java
@@ -1,49 +1,47 @@
-/*
- * 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.util.value;
-
-import java.util.Map;
-
-/**
- * <code>ValueMap</code> for attributes.
- *
- * @author Eelco Hillenius
- * @since 1.2.6
- */
-public final class AttributeMap extends ValueMap
-{
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs an empty <code>AttributeMap</code>.
- */
- public AttributeMap()
- {
- super();
- }
-
- /**
- * Copy constructor.
- *
- * @param map
- * a <code>Map</code> to be copied
- */
- public AttributeMap(final Map<String, Object> map)
- {
- super(map);
- }
-}
+/*
+ * 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.filter;
+
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebPage;
+
+/**
+ * @author svenmeier
+ */
+public class CspNoncePage extends WebPage
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Construct.
+ *
+ * @param parameters
+ */
+ public CspNoncePage()
+ {
+ add(new AjaxEventBehavior("click")
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void onEvent(AjaxRequestTarget target)
+ {
+ }
+ });
+ }
+}
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html
new file mode 100644
index 0000000..baa33ba
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/CspNoncePageExpected.html
@@ -0,0 +1,25 @@
+<html xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
+ <head><meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'nonce-NONCE'; style-src 'nonce-NONCE';" />
+<script type="text/javascript" src="../resource/org.apache.wicket.resource.JQueryResourceReference/jquery/jquery-3.4.1.js" nonce="NONCE"></script>
+<script type="text/javascript" src="../resource/org.apache.wicket.ajax.AbstractDefaultAjaxBehavior/res/js/wicket-ajax-jquery.js" nonce="NONCE"></script>
+<script type="text/javascript" id="wicket-ajax-debug-enable" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Log.enabled=true;
+/*]]>*/
+</script>
+<script type="text/javascript" id="wicket-ajax-base-url" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Ajax.baseUrl="wicket/bookmarkable/org.apache.wicket.markup.head.filter.CspNoncePage?0";
+/*]]>*/
+</script>
+<script type="text/javascript" nonce="NONCE">
+/*<![CDATA[*/
+Wicket.Event.add(window, "domready", function(event) {
+Wicket.Ajax.ajax({"u":"./org.apache.wicket.markup.head.filter.CspNoncePage?0-1.0-","e":"click"});;
+Wicket.Event.publish(Wicket.Event.Topic.AJAX_HANDLERS_BOUND);
+;});
+/*]]>*/
+</script>
+</head><body>
+ </body>
+</html>
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
index 152dfaf..70ea9d8 100644
--- a/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/head/filter/FilteringHeaderResponseTest.java
@@ -94,4 +94,15 @@ class FilteringHeaderResponseTest extends WicketTestCase
response -> new ResourceAggregator(new JavaScriptDeferHeaderResponse(response)));
executeTest(DeferredPage.class, "DeferredPageExpected.html");
}
+
+ /**
+ * WICKET-6682
+ */
+ @Test
+ void nonce() throws Exception
+ {
+ tester.getApplication().setHeaderResponseDecorator(
+ response -> new ResourceAggregator(new CspNonceHeaderResponse(response, "NONCE")));
+ executeTest(CspNoncePage.class, "CspNoncePageExpected.html");
+ }
}
diff --git a/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java b/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
index c5a3089..ab82a41 100644
--- a/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
+++ b/wicket-core/src/test/java/org/apache/wicket/page/XmlPartialPageUpdateTest.java
@@ -48,7 +48,7 @@ class XmlPartialPageUpdateTest extends WicketTestCase
update.writeTo(response, "UTF-8");
- String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ajax-response><component id=\"container1\" ><![CDATA[<span wicket:id=\"container\" id=\"container1\"> two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[> </span>]]></component><header-contribution><![CDATA[<head xmlns:wicket=\"http://wicket.apache.org\"><script type=\"text/javascript\" >\n" +
+ String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ajax-response><component id=\"container1\" ><![CDATA[<span wicket:id=\"container\" id=\"container1\"> two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[> </span>]]></component><header-contribution><![CDATA[<head xmlns:wicket=\"http://wicket.apache.org\"><script type=\"text/javascript\">\n" +
"/*<![CDATA[*/\n" +
"// two brackets: ]] greater than: > CDATA end: ]]]]><![CDATA[>\n" +
"/*]]]]><![CDATA[>*/\n" +
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java
new file mode 100644
index 0000000..b4b8341
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/CspApplication.java
@@ -0,0 +1,72 @@
+/*
+ * 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.examples.csp;
+
+import java.util.Base64;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Page;
+import org.apache.wicket.Session;
+import org.apache.wicket.examples.WicketExampleApplication;
+import org.apache.wicket.markup.head.ResourceAggregator;
+import org.apache.wicket.markup.head.filter.CspNonceHeaderResponse;
+
+public class CspApplication extends WicketExampleApplication
+{
+ private static final int NONCE_LENGTH = 24;
+
+ public static MetaDataKey<String> NONCE_KEY = new MetaDataKey<String>()
+ {
+ };
+
+ @Override
+ public Class<? extends Page> getHomePage()
+ {
+ return NonceDemoPage.class;
+ }
+
+ @Override
+ protected void init()
+ {
+ super.init();
+
+ setHeaderResponseDecorator(response -> new ResourceAggregator(new CspNonceHeaderResponse(response, getNonce())));
+
+ mountPage("noncedemo", NonceDemoPage.class);
+ }
+
+ protected static String generateNonce()
+ {
+ byte[] randomBytes = new byte[NONCE_LENGTH];
+ ThreadLocalRandom.current().nextBytes(randomBytes);
+ return Base64.getUrlEncoder().encodeToString(randomBytes);
+ }
+
+ public static String getNonce()
+ {
+ Session session = Session.get();
+ session.bind();
+ String nonce = session.getMetaData(NONCE_KEY);
+ if (nonce == null)
+ {
+ nonce = generateNonce();
+ session.setMetaData(NONCE_KEY, nonce);
+ }
+ return nonce;
+ }
+}
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html
new file mode 100644
index 0000000..9115256
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.html
@@ -0,0 +1,34 @@
+<html xmlns:wicket="http://wicket.apache.org" xml:lang="en" lang="en">
+<head>
+ <title>CSP Nonce Demo</title>
+</head>
+<body>
+<wicket:extend>
+ <h3>CSP Nonce Demo</h3>
+ <div wicket:id="testNonceScript" class="test-nonce-script"></div>
+ <div wicket:id="testNoNonceScript" class="test-no-nonce-script"></div>
+ <div class="injected-style"><wicket:message key="styleInjectionContainer"/></div>
+ <div class="injected-style--with-nonce"><wicket:message key="styleInjectionContainerWithNonce"/></div>
+ <p></p>
+ <button wicket:id="clickMe"><wicket:message key="clickMe" /></button>
+ <div class="click-me-text">Click a button above to replace this text</div>
+ <div><wicket:message key="clickMeCount" /> <span wicket:id="clickMeCount"></span></div>
+ <p></p>
+ <p><wicket:message key="ieDisclaimer"/></p>
+
+ <!-- Injections below will work in IE11, because IE 11 doesn't support nonce -->
+
+ <!-- script injection (won't work when CSP nonce is in header) -->
+ <script>document.querySelector(".test-no-nonce-script").innerHTML = "This is text injected by no nonce script: fail";</script>
+
+ <!-- style injection (won't work when CSP nonce is in header) -->
+ <style>
+ .injected-style {
+ color: red;
+ font-weight: bold;
+ }
+ </style>
+
+</wicket:extend>
+</body>
+</html>
\ No newline at end of file
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java
new file mode 100644
index 0000000..d092f8d
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.java
@@ -0,0 +1,81 @@
+/*
+ * 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.examples.csp;
+
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.examples.WicketExamplePage;
+import org.apache.wicket.markup.head.CssHeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+/**
+ * Page which disallows execution of inline scripts without nonce
+ */
+public class NonceDemoPage extends WicketExamplePage
+{
+ private final IModel<Integer> clickMeCountModel = Model.of(0);
+
+ public NonceDemoPage()
+ {
+ super();
+ add(new Label("testNonceScript", getString("testNonceScript")));
+ add(new Label("testNoNonceScript", getString("testNoNonceScript")));
+ final Label clickMeCount = new Label("clickMeCount", clickMeCountModel);
+ clickMeCount.setOutputMarkupId(true);
+ add(clickMeCount);
+ add(new AjaxLink<String>("clickMe")
+ {
+ @Override
+ public void onClick(AjaxRequestTarget target)
+ {
+ clickMeCountModel.setObject(clickMeCountModel.getObject() + 1);
+
+ // target.add (works even without unsafe-eval)
+ target.add(clickMeCount);
+ // append javascript (won't work without unsafe-eval)
+ target.appendJavaScript("document.querySelector(\".click-me-text\").innerHTML = \"replaced\";");
+ }
+ }.setOutputMarkupId(true));
+ }
+
+ @Override
+ public void renderHead(IHeaderResponse response)
+ {
+ super.renderHead(response);
+ // Add inline script with nonce
+ response.render(JavaScriptHeaderItem.forScript(
+ "$(function(){$(\".test-nonce-script\").html(\"Text injected by script with nonce: success\");});",
+ "test-nonce-script"
+ ));
+ // Add inline css with nonce
+ response.render(CssHeaderItem.forCSS(
+ ".injected-style--with-nonce{color: green; font-weight: bold;}",
+ "injected-style-with-nonce")
+ );
+ }
+
+ @Override
+ public void detachModels()
+ {
+ super.detachModels();
+ clickMeCountModel.detach();
+ }
+}
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties
new file mode 100644
index 0000000..4d9eb01
--- /dev/null
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/csp/NonceDemoPage.properties
@@ -0,0 +1,21 @@
+# 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.
+testNonceScript = This text should be replaced: fail
+testNoNonceScript = This text should not be replaced: success
+styleInjectionContainer = This text should not be red
+styleInjectionContainerWithNonce = This text should be green
+ieDisclaimer = Results could be different in legacy browsers such as IE. In modern browsers you will experience a few JavaScript errors telling about refused resources.
+clickMe = Click Me
+clickMeCount = Click Me count:
\ No newline at end of file
diff --git a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
index a58b6c1..e0c69bf 100644
--- a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
+++ b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html
@@ -52,6 +52,7 @@
<tr><td align="right"><a href="bean-validation">Bean Validation</a></td><td> - Bean Validation integration (JSR 303)</td></tr>
<tr><td align="right"><a href="videos">HTML5 Media Tags</a></td><td> - Integration for HTML5 Media Tags</td></tr>
<tr><td align="right"><a href="websockets">Web Sockets</a></td><td> - Integration for JSR356 Web Sockets</td></tr>
+ <tr><td align="right"><a href="csp/noncedemo">CSP</a></td><td> - Content Security Protection</td></tr>
</tbody>
</table>
</wicket:extend>
diff --git a/wicket-examples/src/main/webapp/WEB-INF/web.xml b/wicket-examples/src/main/webapp/WEB-INF/web.xml
index e530a05..4364eeb 100644
--- a/wicket-examples/src/main/webapp/WEB-INF/web.xml
+++ b/wicket-examples/src/main/webapp/WEB-INF/web.xml
@@ -258,6 +258,15 @@
</filter>
<filter>
+ <filter-name>CspApplication</filter-name>
+ <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
+ <init-param>
+ <param-name>applicationClassName</param-name>
+ <param-value>org.apache.wicket.examples.csp.CspApplication</param-value>
+ </init-param>
+ </filter>
+
+ <filter>
<filter-name>RepeaterExamplesApplication</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
@@ -645,6 +654,13 @@
</filter-mapping>
<filter-mapping>
+ <filter-name>CspApplication</filter-name>
+ <url-pattern>/csp/*</url-pattern>
+ <dispatcher>REQUEST</dispatcher>
+ <dispatcher>INCLUDE</dispatcher>
+ </filter-mapping>
+
+ <filter-mapping>
<filter-name>FramesApplication</filter-name>
<url-pattern>/frames/*</url-pattern>
</filter-mapping>
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java b/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
index 8a6f297..d2ccec5 100755
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
+++ b/wicket-util/src/main/java/org/apache/wicket/util/value/AttributeMap.java
@@ -18,11 +18,16 @@ package org.apache.wicket.util.value;
import java.util.Map;
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.apache.wicket.util.string.Strings;
+
/**
- * <code>ValueMap</code> for attributes.
+ * Map of values, extending {@link ValueMap} with methods for generating (HTML) markup attributes.
*
* @author Eelco Hillenius
* @since 1.2.6
+ *
+ * @see Strings#escapeMarkup(CharSequence)
*/
public final class AttributeMap extends ValueMap
{
@@ -46,4 +51,84 @@ public final class AttributeMap extends ValueMap
{
super(map);
}
+
+ /**
+ * Put a boolean attribute, removing it if {@code value} is false or using the key as value otherwise,
+ * i.e. {@code value="value"}.
+ *
+ * @param key
+ * key of attribute
+ * @param value
+ * @return previous value
+ */
+ public boolean putAttribute(final String key, final boolean value)
+ {
+ Object previous = get(key);
+ if (value)
+ {
+ put(key, key);
+ }
+ else
+ {
+ remove(key);
+ }
+ return key.equals(previous);
+ }
+
+ /**
+ * Put a string attribute, removing it if the string is empty (nor null).
+ *
+ * @param key
+ * key of attribute
+ * @param value
+ * @return previous value
+ */
+ public String putAttribute(String key, CharSequence value)
+ {
+ if (Strings.isEmpty(value))
+ {
+ return (String)remove(key);
+ }
+ else
+ {
+ return (String)put(key, value);
+ }
+ }
+
+ /**
+ * Representation as encoded markup attributes.
+ *
+ * @see Strings#escapeMarkup(CharSequence)
+ */
+ public String toString()
+ {
+ return toCharSequence().toString();
+ }
+
+ /**
+ * Representation as encoded markup attributes.
+ *
+ * @see Strings#escapeMarkup(CharSequence)
+ */
+ public CharSequence toCharSequence()
+ {
+ final AppendingStringBuffer buffer = new AppendingStringBuffer();
+
+ for (String key : keySet())
+ {
+ if (key != null) {
+ buffer.append(' ');
+ buffer.append(Strings.escapeMarkup(key));
+
+ CharSequence value = getCharSequence(key);
+ if (value != null) {
+ buffer.append("=\"");
+ buffer.append(Strings.escapeMarkup(value));
+ buffer.append('"');
+ }
+ }
+ }
+
+ return buffer;
+ }
}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java b/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
index c0c5e00..ae554ae 100755
--- a/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
+++ b/wicket-util/src/main/java/org/apache/wicket/util/value/ValueMap.java
@@ -28,6 +28,7 @@ import java.util.Map;
import org.apache.wicket.util.parse.metapattern.MetaPattern;
import org.apache.wicket.util.parse.metapattern.parsers.VariableAssignmentParser;
+import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.IStringIterator;
import org.apache.wicket.util.string.StringList;
import org.apache.wicket.util.string.StringValue;
@@ -544,7 +545,7 @@ public class ValueMap extends LinkedHashMap<String, Object> implements IValueMap
@Override
public String toString()
{
- final StringBuilder buffer = new StringBuilder();
+ final AppendingStringBuffer buffer = new AppendingStringBuffer();
boolean first = true;
for (Map.Entry<String, Object> entry : entrySet())
{
diff --git a/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java b/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java
new file mode 100644
index 0000000..e010655
--- /dev/null
+++ b/wicket-util/src/test/java/org/apache/wicket/util/value/AttributeMapTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util.value;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for {@link AttributeMap}.
+ *
+ * @author svenmeier
+ */
+public class AttributeMapTest
+{
+
+ @Test
+ public void encoding() {
+ AttributeMap map = new AttributeMap();
+
+ map.putAttribute("key", "-&-ö-<->-");
+
+ assertEquals(" key=\"-&-ö-<->-\"", map.toString());
+ }
+
+ @Test
+ public void putString()
+ {
+ AttributeMap map = new AttributeMap();
+
+ assertNull(map.putAttribute("foo", "bar"));
+ assertEquals("bar", map.get("foo"));
+
+ assertEquals("bar", map.putAttribute("foo", "baz"));
+ assertEquals("baz", map.get("foo"));
+
+ assertEquals("baz", map.putAttribute("foo", null));
+ assertEquals(null, map.get("foo"));
+ }
+
+ @Test
+ public void putBoolean()
+ {
+ AttributeMap map = new AttributeMap();
+
+ assertFalse(map.putAttribute("foo", true));
+ assertEquals("foo", map.get("foo"));
+
+ assertTrue(map.putAttribute("foo", false));
+ assertEquals(null, map.get("foo"));
+ }
+}
diff --git a/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java b/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
index be3d6b7..4aad486 100644
--- a/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
+++ b/wicket-util/src/test/java/org/apache/wicket/util/value/ValueMapTest.java
@@ -244,6 +244,9 @@ public class ValueMapTest
assertEquals(defDuration, vm.getAsDuration("duration.bad", defDuration));
assertNull(vm.getAsDuration("duration.missing"));
assertEquals(defDuration, vm.getAsDuration("duration.missing", defDuration));
+
+ vm.remove("instant");
+ assertEquals("num = \"42\" num.bad = \"xxx\" instant.bad = \"xxx\" duration = \"PT1H\" duration.bad = \"xxx\" boolean = \"true\" boolean.bad = \"xxx\"", vm.toString());
}
/**