/* HTMLs.java
Purpose:
Description:
History:
Sat Dec 31 12:46:27 2005, Created by tomyeh
Copyright (C) 2004 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 3.0 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.html;
import java.util.Set;
import java.util.HashSet;
import org.zkoss.xml.XMLs;
/**
* Utilities for HTML attributes and styles.
*
* @author tomyeh
* @since 6.0.0
*/
public class HTMLs {
/** Appends an attribute to the string buffer for HTML/XML (name="val").
* If val is null or empty (if String), nothing is generated.
*
* <p>Note: {@link XMLs#encodeAttribute} is called automatically
* to encode val.
*/
public static final
void appendAttribute(StringBuffer sb, String name, String val) {
if (val != null && val.length() != 0)
sb.append(' ').append(name).append("=\"")
.append(XMLs.encodeAttribute(val)).append('"');
}
/** Appends an attribute to the string buffer for HTML/XML (name="val").
* If emptyIgnored is true and val is null or empty (if String),
* nothing is generated.
*
* <p>Note: {@link XMLs#encodeAttribute} is called automatically
* to encode val.
*
* @param emptyIgnored whether to ignore a null or empty string.
* If false, it is always generated (null is generated as "null").
*/
public static final
void appendAttribute(StringBuffer sb, String name, String val,
boolean emptyIgnored) {
if (!emptyIgnored || (val != null && val.length() != 0))
sb.append(' ').append(name).append("=\"")
.append(val != null ? XMLs.encodeAttribute(val): null)
.append('"');
}
/** Appends an attribute with a int value to the string buffer for HTML/XML (name="val").
*/
public static final
void appendAttribute(StringBuffer sb, String name, int val) {
sb.append(' ').append(name).append("=\"").append(val).append('"');
}
/** Appends an attribute with a long value to the string buffer for HTML/XML (name="val").
*/
public static final
void appendAttribute(StringBuffer sb, String name, long val) {
sb.append(' ').append(name).append("=\"").append(val).append('"');
}
/** Appends an attribute with a long value to the string buffer for HTML/XML (name="val").
*/
public static final
void appendAttribute(StringBuffer sb, String name, double val) {
sb.append(' ').append(name).append("=\"").append(val).append('"');
}
/** Appends an attribute with a short value to the string buffer for HTML/XML (name="val").
*/
public static final
void appendAttribute(StringBuffer sb, String name, short val) {
sb.append(' ').append(name).append("=\"").append(val).append('"');
}
/** Appends an attribute to the string buffer for HTML/XML (name="val").
*/
public static final
void appendAttribute(StringBuffer sb, String name, boolean val) {
sb.append(' ').append(name).append("=\"").append(val).append('"');
}
/** Appends a style value to the string buffer for HTML/XML (name:"val";).
* If val is null or empty (if String), nothing is generated.
*/
public static final
void appendStyle(StringBuffer sb, String name, String val) {
if (val != null && val.length() != 0)
sb.append(name).append(':').append(val).append(';');
}
/** Returns the position of the specified substyle, or -1 if not found.
*
* @param style the style
* @param substyle the sub-style, e.g., display.
* @exception IllegalArgumentException if style is null, or substyle is null
* or empty.
*/
public static final int getSubstyleIndex(String style, String substyle) {
if (style == null || substyle == null)
throw new IllegalArgumentException("null");
if (substyle.length() == 0)
throw new IllegalArgumentException("empty substyle");
for (int j = 0, len = style.length();;) {
int k = -1, l = j;
for (; l < len; ++l) {
final char cc = style.charAt(l);
if (k < 0 && cc == ':') k = l; //colon found
else if (cc == ';') break; //done
}
final String nm =
(k >= 0 ? style.substring(j, k): style.substring(j, l)).trim();
if (nm.equals(substyle)) return j;
if (l >= len) return -1;
j = l + 1;
}
}
/** Returns the value starting at the specified index (never null).
*
* <p>Note: the index is usually the returned vale of {@link #getSubstyleIndex}.
*
* @param style the style
* @param j the index that the substyle starts at (including the style's name)
*/
public static final String getSubstyleValue(final String style, int j) {
final int len = style.length();
int k = -1, l = j;
for (; l < len; ++l) {
final char cc = style.charAt(l);
if (k < 0 && cc == ':') k = l; //colon found
else if (cc == ';') break; //done
}
return k < 0 ? "": style.substring(k + 1, l).trim();
}
/** Retrieves text relevant CSS styles.
*
* <p>For example, if style is
* "border: 1px solid blue; font-size: 10px; padding: 3px; color: black;",
* then "font-size: 10px;color: black;" is returned.
*
* @return null if style is null. Otherwise, it never returns null.
*/
public static final String getTextRelevantStyle(final String style) {
if (style == null) return null;
if (style.length() == 0) return "";
final StringBuffer sb = new StringBuffer(64);
for (int j = 0, len = style.length();;) {
int k = -1, l = j;
for (; l < len; ++l) {
final char cc = style.charAt(l);
if (k < 0 && cc == ':') k = l; //colon found
else if (cc == ';') break; //done
}
final String nm =
(k >= 0 ? style.substring(j, k): style.substring(j, l)).trim();
if (nm.startsWith("font") || nm.startsWith("text")
|| _txtstyles.contains(nm))
sb.append(
l < len ? style.substring(j, l + 1): style.substring(j));
if (l >= len) return sb.toString();
j = l + 1;
}
}
private final static Set<String> _txtstyles;
static {
final String[] txts = {
"color", "background-color", "background", "white-space"
};
_txtstyles = new HashSet<String>();
for (int j = txts.length; --j >=0;)
_txtstyles.add(txts[j]);
}
/** Returns whether the specified tag is an 'orphan' tag.
* By orphan we mean it doesn't support the format of
* <xx> </xx>.
*
* <p>For example, br and img are orphan tags.
*
* @param tagname the tag name, e.g., br and tr.
*/
public static final boolean isOrphanTag(String tagname) {
return _orphans.contains(tagname.toLowerCase(java.util.Locale.ENGLISH));
}
/** A set of tags that don't have child. */
private static final Set<String> _orphans = new HashSet<String>(29);
static {
final String[] orphans = {
"area", "base", "basefont", "bgsound", "br",
"col", "embed", "hr", "img", "input",
"isindex", "keygen", "link", "meta", "plaintext",
"spacer", "wbr"
};
for (int j = orphans.length; --j >= 0;)
_orphans.add(orphans[j]);
}
// Since 7.0.2
// The following implementation for encoding JavaScript is referred from
// https://code.google.com/p/owasp-esapi-java/
// which is licensed under New BSD License - http://opensource.org/licenses/BSD-3-Clause
private static char[] IMMUNE_JAVASCRIPT = { ',', '.', '_' };
private static final String[] hex = new String[256];
private static boolean containsCharacter(char c, char[] array) {
for (char ch : array) {
if (c == ch)
return true;
}
return false;
}
static {
for (char c = 0; c < 0xFF; c++) {
if (c >= 0x30 && c <= 0x39 || c >= 0x41 && c <= 0x5A || c >= 0x61
&& c <= 0x7A) {
hex[c] = null;
} else {
hex[c] = toHex(c).intern();
}
}
}
private static String toHex(char c) {
return Integer.toHexString(c);
}
private static String getHexForNonAlphanumeric(char c) {
if (c < 0xFF)
return hex[c];
return toHex(c);
}
/**
* Encodes the JavaScript content for <a href=
* "https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.233_-_JavaScript_Escape_Before_Inserting_Untrusted_Data_into_JavaScript_Data_Values"
* > XSS vulnerabilities</a>, the implementation is referred from <a
* href="https://code.google.com/p/owasp-esapi-java/">owasp-esapi-java</a>
* <p>
* Returns backslash encoded numeric format. Does not use backslash
* character escapes such as, \" or \' as these may cause parsing problems.
* For example, if a javascript attribute, such as onmouseover, contains a
* \" that will close the entire attribute and allow an attacker to inject
* another script attribute.
*
* @since 7.0.2
*/
public static String encodeJavaScript(String input) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
sb.append(encodeCharacter(IMMUNE_JAVASCRIPT, c));
}
return sb.toString();
}
/**
* Encodes the JavaScript content for <a href=
* "https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.233_-_JavaScript_Escape_Before_Inserting_Untrusted_Data_into_JavaScript_Data_Values"
* > XSS vulnerabilities</a>, the implementation is referred from <a
* href="https://code.google.com/p/owasp-esapi-java/">owasp-esapi-java</a>
* <p>
* Returns backslash encoded numeric format. Does not use backslash
* character escapes such as, \" or \' as these may cause parsing problems.
* For example, if a javascript attribute, such as onmouseover, contains a
* \" that will close the entire attribute and allow an attacker to inject
* another script attribute.
*
* @since 7.0.2
*/
public static String encodeCharacter(char[] immune, Character c) {
// check for immune characters
if (containsCharacter(c, immune)) {
return "" + c;
}
// check for alphanumeric characters
String hex = getHexForNonAlphanumeric(c);
if (hex == null) {
return "" + c;
}
// Do not use these shortcuts as they can be used to break out of a
// context
// if ( ch == 0x00 ) return "\\0";
// if ( ch == 0x08 ) return "\\b";
// if ( ch == 0x09 ) return "\\t";
// if ( ch == 0x0a ) return "\\n";
// if ( ch == 0x0b ) return "\\v";
// if ( ch == 0x0c ) return "\\f";
// if ( ch == 0x0d ) return "\\r";
// if ( ch == 0x22 ) return "\\\"";
// if ( ch == 0x27 ) return "\\'";
// if ( ch == 0x5c ) return "\\\\";
// encode up to 256 with \\xHH
String temp = Integer.toHexString(c);
if (c < 256) {
String pad = "00".substring(temp.length());
return "\\x" + pad + temp.toUpperCase();
}
// otherwise encode with \\uHHHH
String pad = "0000".substring(temp.length());
return "\\u" + pad + temp.toUpperCase();
}
}