wgt.setAttr("value2")
will be invoked at the client
* accordingly.
*
+ * Note: the String type value will be escaped by {@link Strings#escapeJavaScript} and {@link XMLs#escapeXML}. + * To allow the safe HTML, use {@link org.zkoss.zk.ui.SafeHtmlValue} to wrap the value. (since 10.0.0) * @param append whether to append the updates of properties with the same * name. If false, only the last value of the same property will be sent * to the client. @@ -1860,8 +1863,13 @@ protected void smartUpdate(String attr, Object value) { * @see #smartUpdate(String, Object) */ protected void smartUpdate(String attr, Object value, boolean append) { - if (_page != null) + if (_page != null) { + if (value instanceof String) { + // no need to use Strings.escapeJavaScript() here. + value = XMLs.escapeXML((String) value); + } getAttachedUiEngine().addSmartUpdate(this, attr, value, append); + } } /** A special smart update to update a value in int. diff --git a/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java b/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java index ba5144661a2..ca3156b795f 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java +++ b/zk/src/main/java/org/zkoss/zk/ui/HtmlNativeComponent.java @@ -294,8 +294,8 @@ private static boolean startsWith(StringBuffer sb, String tag, int start) { protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException { super.renderProperties(renderer); - render(renderer, "prolog", getPrologHalf()); - render(renderer, "epilog", getEpilogHalf()); + render(renderer, "prolog", SafeHtmlValue.valueOf(getPrologHalf())); + render(renderer, "epilog", SafeHtmlValue.valueOf(getEpilogHalf())); } private String getPrologHalf() { diff --git a/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java b/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java new file mode 100644 index 00000000000..09c30987744 --- /dev/null +++ b/zk/src/main/java/org/zkoss/zk/ui/SafeHtmlValue.java @@ -0,0 +1,78 @@ +/* SafeHtmlValue.java + + Purpose: + + Description: + + History: + 2:39 PM 2023/12/20, Created by jumperchen + +Copyright (C) 2023 Potix Corporation. All Rights Reserved. +*/ +package org.zkoss.zk.ui; + +import java.util.Objects; + +import org.zkoss.json.JSONValue; + +/** + * A string-like value that is safe to be used as HTML content. + * @author jumperchen + * @since 10.0.0 + */ +public class SafeHtmlValue implements org.zkoss.json.JSONAware, java.io.Serializable { + private static final long serialVersionUID = 202312201440L; + private final String _value; + + /** An empty SafeHtmlValue instance. */ + public static final SafeHtmlValue EMPTY = new SafeHtmlValue(""); + + /** + * Constructor. + * @param value the value to be wrapped. + */ + public SafeHtmlValue(String value) { + _value = value; + } + + /** + * Returns the wrapped value. + */ + public String getValue() { + return _value; + } + + @Override + public String toString() { + return _value; + } + + @Override + public String toJSONString() { + return JSONValue.toJSONString(_value); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SafeHtmlValue that = (SafeHtmlValue) o; + return Objects.equals(_value, that._value); + } + + @Override + public int hashCode() { + return Objects.hash(_value); + } + + /** + * Returns a SafeHtmlValue instance for the specified value. + * @param value the value to be wrapped. + * @since 10.0.0 + */ + public static SafeHtmlValue valueOf(String value) { + return new SafeHtmlValue(value); + } +} diff --git a/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java b/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java index 615233dceaa..7a858246dc5 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java +++ b/zk/src/main/java/org/zkoss/zk/ui/sys/JSCumulativeContentRenderer.java @@ -23,6 +23,7 @@ import org.zkoss.json.JSONs; import org.zkoss.lang.Generics; import org.zkoss.lang.Strings; +import org.zkoss.xml.XMLs; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.UiException; @@ -95,7 +96,7 @@ private String renderValue(String value) { if (value == null) return null; else { - return Strings.escapeJavaScript(value); + return Strings.escapeJavaScript(XMLs.escapeXML(value)); } } diff --git a/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java b/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java index 873a75c461e..1784ec5b948 100644 --- a/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java +++ b/zk/src/main/java/org/zkoss/zk/ui/sys/JsContentRenderer.java @@ -25,6 +25,7 @@ import org.zkoss.json.JSONAware; import org.zkoss.json.JSONs; import org.zkoss.lang.Strings; +import org.zkoss.xml.XMLs; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.UiException; @@ -153,7 +154,7 @@ private void renderValue(String value) { _buf.append((String) null); else { _buf.append('\''); - _buf.append(Strings.escapeJavaScript(value)); + _buf.append(Strings.escapeJavaScript(XMLs.escapeXML(value))); _buf.append('\''); } } diff --git a/zk/src/main/resources/web/js/zk/utl.ts b/zk/src/main/resources/web/js/zk/utl.ts index 47e9566ca90..e1e7f07ba3e 100644 --- a/zk/src/main/resources/web/js/zk/utl.ts +++ b/zk/src/main/resources/web/js/zk/utl.ts @@ -113,6 +113,7 @@ export interface EncodeXmlOptions { pre?: boolean; multiline?: boolean; maxlength?: number; + encode?: boolean; } export interface ProgressboxOptions { @@ -241,6 +242,7 @@ export namespace utl_global { *
It is useful to show the description in more versatile way. * - *
Default: empty (""). - * - *
Deriving class can override it to return whatever it wants - * other than null. + * @see #getDescription + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { + return _content; + } + + /** Sets the embedded content that is + * shown as part of the description. * *
Unlike other methods, the content assigned to this method - * is generated directly to the browser without escaping. - * Thus, it is better not to have something input by the user to avoid - * any XSS - * attach. + *
Since 10.0.0, the content assigned to this method will be escaped by default. + * To avoid escaping, use {@link #setContent(SafeHtmlValue)} instead. * @see #setDescription * @since 3.0.0 */ public void setContent(String content) { if (content == null) content = ""; + content = Strings.escapeJavaScript(XMLs.escapeXML(content)); + if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) { + _content = SafeHtmlValue.valueOf(content); + smartUpdate("content", getRawContent()); + } + } + + /** Sets the embedded content (i.e., HTML tags) that is + * shown as part of the description. + * @since 10.0.0 + */ + public void setContent(SafeHtmlValue content) { + if (content == null) + content = SafeHtmlValue.EMPTY; if (!Objects.equals(_content, content)) { _content = content; - smartUpdate("content", getContent()); //allow overriding getContent() + smartUpdate("content", getRawContent()); } } @@ -203,7 +222,7 @@ protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) th render(renderer, "disabled", _disabled); render(renderer, "description", getDescription()); //allow overriding getDescription() - render(renderer, "content", getContent()); //allow overriding getContent() + render(renderer, "content", getRawContent()); //allow overriding getContent() if (_value instanceof String) { render(renderer, "value", _value); diff --git a/zul/src/main/java/org/zkoss/zul/Html.java b/zul/src/main/java/org/zkoss/zul/Html.java index 1b88e765940..2e422537185 100644 --- a/zul/src/main/java/org/zkoss/zul/Html.java +++ b/zul/src/main/java/org/zkoss/zul/Html.java @@ -20,6 +20,7 @@ import org.zkoss.json.JavaScriptValue; import org.zkoss.lang.Objects; +import org.zkoss.zk.ui.SafeHtmlValue; import org.zkoss.zk.ui.sys.HtmlPageRenders; import org.zkoss.zul.impl.XulElement; @@ -72,7 +73,7 @@ * @author tomyeh */ public class Html extends XulElement { - private String _content = ""; + private SafeHtmlValue _content = SafeHtmlValue.EMPTY; /** Constructs a {@link Html} component to embed HTML tags. */ @@ -83,12 +84,19 @@ public Html() { * with the specified content. */ public Html(String content) { - _content = content != null ? content : ""; + _content = SafeHtmlValue.valueOf(content != null ? content : ""); } /** Returns the embedded content (i.e., HTML tags). */ public String getContent() { + return _content.toString(); + } + + /** Returns the embedded content (i.e., HTML tags). + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { return _content; } @@ -107,10 +115,9 @@ public String getContent() { public void setContent(String content) { if (content == null) content = ""; - if (!Objects.equals(_content, content)) { - _content = content; - smartUpdate("content", getContent()); - //allow deriving to override getContent() + if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) { + _content = SafeHtmlValue.valueOf(content); + smartUpdate("content", getRawContent()); } } @@ -135,7 +142,7 @@ protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) th if (cnt == null) renderer.render("content", new JavaScriptValue("zk('" + getUuid() + "').detachChildren()")); else - render(renderer, "content", cnt); + render(renderer, "content", getRawContent()); } } diff --git a/zul/src/main/java/org/zkoss/zul/Menu.java b/zul/src/main/java/org/zkoss/zul/Menu.java index c4d5ff401b0..7353267ea89 100644 --- a/zul/src/main/java/org/zkoss/zul/Menu.java +++ b/zul/src/main/java/org/zkoss/zul/Menu.java @@ -20,9 +20,12 @@ import java.util.Map; import org.zkoss.lang.Objects; +import org.zkoss.lang.Strings; +import org.zkoss.xml.XMLs; import org.zkoss.zk.au.AuRequest; import org.zkoss.zk.au.out.AuInvoke; import org.zkoss.zk.ui.Component; +import org.zkoss.zk.ui.SafeHtmlValue; import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.event.InputEvent; @@ -41,7 +44,7 @@ */ public class Menu extends LabelImageElement implements org.zkoss.zk.ui.ext.Disable { private Menupopup _popup; - private String _content = ""; + private SafeHtmlValue _content = SafeHtmlValue.EMPTY; private boolean _disabled = false; static { @@ -83,10 +86,22 @@ public Menupopup getMenupopup() { * @since 5.0.0 */ public String getContent() { + return _content.toString(); + } + + /** Returns the embedded content (i.e., HTML tags) that is + * shown as part of the description. + * + *
It is useful to show the description in more versatile way. + * + * @since 10.0.0 + */ + public SafeHtmlValue getRawContent() { return _content; } - /** Sets the embedded content (i.e., HTML tags) that is + + /** Sets the embedded content that is * shown as part of the description. * *
It is useful to show the description in more versatile way. @@ -94,14 +109,32 @@ public String getContent() { *
There is a way to create Colorbox automatically by using
* #color=#RRGGBB, usage example setContent("#color=#FFFFFF")
*
+ *
Since 10.0.0, the content assigned to this method will be escaped by default.
+ * To avoid escaping, use {@link #setContent(SafeHtmlValue)} instead.
* @since 5.0.0
*/
public void setContent(String content) {
if (content == null)
content = "";
+ content = Strings.escapeJavaScript(XMLs.escapeXML(content));
+ if (!Objects.equals(_content, SafeHtmlValue.valueOf(content))) {
+ _content = SafeHtmlValue.valueOf(content);
+ smartUpdate("content", getRawContent());
+ }
+ }
+
+
+ /** Sets the embedded content (i.e., HTML tags) that is
+ * shown as part of the description.
+ * @since 10.0.0
+ */
+ public void setContent(SafeHtmlValue content) {
+ if (content == null)
+ content = SafeHtmlValue.EMPTY;
if (!Objects.equals(_content, content)) {
_content = content;
- smartUpdate("content", content);
+ smartUpdate("content", getRawContent());
}
}
@@ -141,7 +174,7 @@ public String getZclass() {
protected void renderProperties(ContentRenderer renderer) throws IOException {
super.renderProperties(renderer);
- render(renderer, "content", _content);
+ render(renderer, "content", getRawContent());
render(renderer, "disabled", _disabled);
}
diff --git a/zul/src/main/java/org/zkoss/zul/Selectbox.java b/zul/src/main/java/org/zkoss/zul/Selectbox.java
index 27769850281..19946b52c2b 100644
--- a/zul/src/main/java/org/zkoss/zul/Selectbox.java
+++ b/zul/src/main/java/org/zkoss/zul/Selectbox.java
@@ -27,6 +27,7 @@
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.Page;
+import org.zkoss.zk.ui.SafeHtmlValue;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
@@ -63,7 +64,7 @@ public class Selectbox extends HtmlBasedComponent implements Disable {
private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.onInitLaterPosted";
private transient boolean _childable;
- private transient String[] _tmpdatas;
+ private transient SafeHtmlValue[] _tmpdatas;
static {
addClientEvent(Selectbox.class, Events.ON_SELECT, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
@@ -279,7 +280,7 @@ public void onInitRender() {
public void onInitRenderNow() {
if (_model != null) {
- _tmpdatas = new String[_model.getSize()];
+ _tmpdatas = new SafeHtmlValue[_model.getSize()];
final boolean old = _childable;
try {
_childable = true;
@@ -290,7 +291,7 @@ public void onInitRenderNow() {
final Object value = _model.getElementAt(i);
if (_jsel < 0 && smodel.isSelected(value))
_jsel = i;
- _tmpdatas[i] = renderer.render(this, value, i);
+ _tmpdatas[i] = SafeHtmlValue.valueOf(renderer.render(this, value, i));
}
} catch (Exception e) {
throw UiException.Aide.wrap(e);
diff --git a/zul/src/main/resources/web/js/zul/inp/InputWidget.ts b/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
index 6a58e49b321..bb5019a7c91 100644
--- a/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
+++ b/zul/src/main/resources/web/js/zul/inp/InputWidget.ts
@@ -629,7 +629,7 @@ export class InputWidget