/* Encodes.java Purpose: Description: History: Fri Jun 21 14:13:50 2002, Created by tomyeh Copyright (C) 2002 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.web.servlet.http; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zkoss.lang.Classes; import org.zkoss.lang.Library; import org.zkoss.lang.Objects; import org.zkoss.lang.SystemException; import org.zkoss.web.servlet.Charsets; import org.zkoss.web.servlet.Servlets; import org.zkoss.web.util.resource.ExtendletContext; /** * Encoding utilities for servlets. * * @author tomyeh * @see Https */ public class Encodes { private static final Logger log = LoggerFactory.getLogger(Encodes.class); protected Encodes() { } //prevent from instantiation /** Encodes a string to HTTP URI compliance by use of * {@link Charsets#getURICharset}. * * <p>Besides two-byte characters, it also encodes any character found * in unsafes. * * @param unsafes the set of characters that must be encoded; never null. * It must be sorted. */ private static final String encodeURI0(String s, char[] unsafes) throws UnsupportedEncodingException { if (s == null) return null; final String charset = Charsets.getURICharset(); final byte[] in = s.getBytes(charset); final byte[] out = new byte[in.length * 3]; //at most: %xx int j = 0, k = 0; for (; j < in.length; ++j) { //Though it is ok to use '+' for ' ', Jetty has problem to //handle space between Chinese characters. final char cc = (char) (((int) in[j]) & 0xff); if (cc >= 0x80 || cc <= ' ' || Arrays.binarySearch(unsafes, cc) >= 0) { out[k++] = (byte) '%'; String cvt = Integer.toHexString(cc); if (cvt.length() == 1) { out[k++] = (byte) '0'; out[k++] = (byte) cvt.charAt(0); } else { out[k++] = (byte) cvt.charAt(0); out[k++] = (byte) cvt.charAt(1); } } else { out[k++] = in[j]; } } return j == k ? s : new String(out, 0, k, charset); } /** unsafe character when that are used in url's location. */ private static final char[] URI_UNSAFE; /** unsafe character when that are used in url's query. */ private static final char[] URI_COMP_UNSAFE; static { URI_UNSAFE = "`%^{}[]\\\"<>|".toCharArray(); Arrays.sort(URI_UNSAFE); URI_COMP_UNSAFE = "`%^{}[]\\\"<>|$&,/:;=?".toCharArray(); Arrays.sort(URI_COMP_UNSAFE); } /** Does the HTTP encoding for the URI location. * For example, '%' is translated to '%25'. * * <p>Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)} * will invoke this method automatically, you rarely need this method. * * @param s the string to encode; null is OK * @return the encoded string or null if s is null * @see #encodeURIComponent */ public static final String encodeURI(String s) throws UnsupportedEncodingException { return encodeURI0(s, URI_UNSAFE); } /** Does the HTTP encoding for an URI query parameter. * For example, '/' is translated to '%2F'. * Both name and value must be encoded separately. Example, * <code>encodeURIComponent(name) + '=' + encodeURIComponent(value)</code>. * * <p>Since {@link #encodeURL(ServletContext, ServletRequest, ServletResponse, String)} * will <i>not</i> invoke this method automatically, you'd better * to encode each query parameter by this method or * {@link #addToQueryString(StringBuffer,Map)}. * * @param s the string to encode; null is OK * @return the encoded string or null if s is null * @see #addToQueryString(StringBuffer,String,Object) * @see #encodeURI */ public static final String encodeURIComponent(String s) throws UnsupportedEncodingException { return encodeURI0(s, URI_COMP_UNSAFE); } /** /** Appends a map of parameters (name=value) to a query string. * It returns the query string preceding with '?' if any parameter exists, * or an empty string if no parameter at all. Thus, you could do * * <p><code>request.getRequestDispatcher(<br> * addToQueryStirng(new StringBuffer(uri), params).toString());</code> * * <p>Since RequestDispatcher.include and forward do not allow wrapping * the request and response -- see spec and the implementation of both * Jetty and Catalina, we have to use this method to pass the parameters. * * @param params a map of parameters; format: (String, Object) or * (String, Object[]); null is OK */ public static final StringBuffer addToQueryString(StringBuffer sb, Map params) throws UnsupportedEncodingException { if (params != null) { for (Iterator it = params.entrySet().iterator(); it.hasNext();) { final Map.Entry me = (Map.Entry) it.next(); addToQueryString(sb, (String) me.getKey(), me.getValue()); } } return sb; } /** Appends a parameter (name=value) to a query string. * This method automatically detects whether other query is already * appended. If so, & is used instead of ?. * * <p>The query string might contain servlet path and other parts. * This method starts the searching from the first '?'. * If the query string doesn't contain '?', it is assumed to be a string * without query's name/value pairs. * * @param value the value. If it is null, only name is appended. * If it is an array of objects, multiple pairs of name=value[j] will * be appended. */ public static final StringBuffer addToQueryString(StringBuffer sb, String name, Object value) throws UnsupportedEncodingException { if (value instanceof Object[]) { final Object[] vals = (Object[]) value; if (vals.length == 0) { value = null; //only append name } else { for (int j = 0; j < vals.length; ++j) addToQueryString(sb, name, vals[j]); return sb; //done } } sb.append(next(sb, '?', 0) >= sb.length() ? '?' : '&'); sb.append(encodeURIComponent(name)).append('='); //NOTE: jetty with jboss3.0.6 ignore parameters without '=', //so we always append '=' even value is null if (value != null) sb.append(encodeURIComponent(Objects.toString(value))); return sb; } /** Returns the first occurrence starting from j, or sb.length(). */ private static final int next(StringBuffer sb, char cc, int j) { for (final int len = sb.length(); j < len; ++j) if (sb.charAt(j) == cc) break; return j; } /** * Sets the parameter (name=value) to a query string. * If the name already exists in the query string, it will be removed first. * If your name has appeared in the string, it will replace * your new value to the query string. * Otherwise, it will append the name/value to the new string. * * <p>The query string might contain servlet path and other parts. * This method starts the searching from the first '?'. * If the query string doesn't contain '?', it is assumed to be a string * without query's name/value pairs. * * @param str The query string like xxx?xxx=xxx&xxx=xxx or null. * @param name The get parameter name. * @param value The value associated with the get parameter name. * @return The new or result query string with your name/value. * @see #addToQueryString */ public static final String setToQueryString(String str, String name, Object value) throws UnsupportedEncodingException { final StringBuffer sb = new StringBuffer(); if (str != null) sb.append(str); return setToQueryString(sb, name, value).toString(); } /** * Sets the parameter (name=value) to a query string. * If the name already exists in the query string, it will be * removed first. * @see #addToQueryString */ public static final StringBuffer setToQueryString(StringBuffer sb, String name, Object value) throws UnsupportedEncodingException { removeFromQueryString(sb, name); return addToQueryString(sb, name, value); } /** * Sets a map of parameters (name=value) to a query string. * If the name already exists in the query string, it will be removed first. * @see #addToQueryString */ public static final String setToQueryString(String str, Map params) throws UnsupportedEncodingException { return setToQueryString(new StringBuffer(str), params).toString(); } /** * Sets a map of parameters (name=value) to a query string. * If the name already exists in the query string, it will be removed first. * @see #addToQueryString */ public static final StringBuffer setToQueryString(StringBuffer sb, Map params) throws UnsupportedEncodingException { if (params != null) { for (Iterator it = params.entrySet().iterator(); it.hasNext();) { final Map.Entry me = (Map.Entry) it.next(); setToQueryString(sb, (String) me.getKey(), me.getValue()); } } return sb; } /** Tests whether a parameter exists in the query string. */ public static final boolean containsQuery(String str, String name) { int j = str.indexOf(name); if (j <= 0) return false; char cc = str.charAt(j - 1); if (cc != '?' && cc != '&') return false; j += name.length(); if (j >= str.length()) return true; cc = str.charAt(j); return cc == '=' || cc == '&'; } /** Remove all name/value pairs of the specified name from a string. * * <p>The query string might contain servlet path and other parts. * This method starts the searching from the last '?'. * If the query string doesn't contain '?', it is assumed to be a string * without query's name/value pairs. * @see #addToQueryString */ public static final String removeFromQueryString(String str, String name) throws UnsupportedEncodingException { if (str == null) return null; int j = str.indexOf('?'); if (j < 0) return str; final StringBuffer sb = new StringBuffer(str); removeFromQueryString(sb, name); return sb.length() == str.length() ? str : sb.toString(); } /** Remove all name/value pairs of the specified name from a string. * @see #addToQueryString */ public static final StringBuffer removeFromQueryString(StringBuffer sb, String name) throws UnsupportedEncodingException { name = encodeURIComponent(name); int j = sb.indexOf("?"); if (j < 0) return sb; //no '?' j = sb.indexOf(name, j + 1); if (j < 0) return sb; //no name final int len = name.length(); do { //1. make sure left is & or ? int k = j + len; char cc = sb.charAt(j - 1); if (cc != '&' && cc != '?') { j = k; continue; } //2. make sure right is = or & or end-of-string if (k < sb.length()) { cc = sb.charAt(k); if (cc != '=' && cc != '&') { j = k; continue; } } //3. remove it until next & or end-of-string k = next(sb, '&', k); if (k < sb.length()) sb.delete(j, k + 1); //also remove '&' else sb.delete(j - 1, k); //also preceding '?' or '&' } while ((j = sb.indexOf(name, j)) > 0); return sb; } /** Encodes an URL. * It resolves "*" contained in URI, if any, to the proper Locale, * and the browser code. * Refer to {@link Servlets#locate(ServletContext, ServletRequest, String, Locator)} * for details. * * <p>In additions, if uri starts with "/", the context path, e.g., * /zkdemo, is prefixed. * In other words, "/ab/cd" means it is relevant to the servlet * context path (say, "/zkdemo"). * * <p>If uri starts with "~abc/", it means it is relevant to a foreign * Web context called /abc. And, it will be converted to "/abc/" first * (without prefix request.getContextPath()). * * <p>Finally, the uri is encoded by HttpServletResponse.encodeURL. * * <p>This method invokes {@link #encodeURI} for any characters * before '?'. However, it does NOT encode any character after '?'. Thus, * you might have to invoke * {@link #encodeURIComponent} or {@link #addToQueryString(StringBuffer,Map)} * to encode the query parameters. * * <h3>The URL Prefix and Encoder</h3> * * <p>In a sophisticated environment, e.g., * <a href="http://en.wikipedia.org/wiki/Reverse_proxy">Reverse Proxy</a>, * the encoded URL might have to be prefixed with some special prefix. * To do that, you can implement {@link URLEncoder}, and then * specify the class with the library property called * <code>org.zkoss.web.servlet.http.URLEncoder</code>. * When {@link #encodeURL} encodes an URL, it will invoke * {@link URLEncoder#encodeURL} such you can do customized encoding, * such as insert a special prefix. * * @param request the request; never null * @param response the response; never null * @param uri it must be null, empty or starts with "/". It might contain * "*" for current browser code and Locale. * @param ctx the servlet context; used only if "*" is contained in uri * @exception IndexOutOfBoundException if uri is empty * @see org.zkoss.web.servlet.Servlets#locate * @see org.zkoss.web.servlet.Servlets#generateURI */ public static final String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response, String uri) throws ServletException { try { return urlEncoder().encodeURL(ctx, request, response, uri, _urlenc0); } catch (Exception ex) { log.error("", ex); throw new ServletException("Unable to encode " + uri, ex); } } private static URLEncoder urlEncoder() { if (_urlenc == null) { final String cls = Library.getProperty("org.zkoss.web.servlet.http.URLEncoder"); if (cls != null && cls.length() > 0) { try { _urlenc = (URLEncoder) Classes.newInstanceByThread(cls); } catch (Throwable ex) { throw SystemException.Aide.wrap(ex, "Unable to instantiate " + cls); } } else { _urlenc = _urlenc0; } } return _urlenc; } private static URLEncoder _urlenc; private static final URLEncoder _urlenc0 = new URLEncoder() { public String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response, String uri, URLEncoder defaultEncoder) throws Exception { return encodeURL0(ctx, request, response, uri); } }; private static final String encodeURL0(ServletContext ctx, ServletRequest request, ServletResponse response, String uri) throws Exception { if (uri == null || uri.length() == 0) return uri; //keep as it is boolean ctxpathSpecified = false; if (uri.charAt(0) != '/') { //NOT relative to context path if (Servlets.isUniversalURL(uri)) return uri; //nothing to do if (uri.charAt(0) == '~') { //foreign context final String ctxroot; if (uri.length() == 1) { ctxroot = uri = "/"; } else if (uri.charAt(1) == '/') { ctxroot = "/"; uri = uri.substring(1); } else { uri = '/' + uri.substring(1); final int j = uri.indexOf('/', 1); ctxroot = j >= 0 ? uri.substring(0, j) : uri; } final ExtendletContext extctx = Servlets.getExtendletContext(ctx, ctxroot.substring(1)); if (extctx != null) { final int j = uri.indexOf('/', 1); return extctx.encodeURL(request, response, j >= 0 ? uri.substring(j) : "/"); } final ServletContext newctx = ctx.getContext(ctxroot); if (newctx != null) { ctx = newctx; } else if (log.isDebugEnabled()) { log.debug("Context not found: " + ctxroot); } ctxpathSpecified = true; } else if (Https.isIncluded(request) || Https.isForwarded(request)) { //if relative URI and being included/forwarded, //converts to absolute String pgpath = Https.getThisServletPath(request); if (pgpath != null) { int j = pgpath.lastIndexOf('/'); if (j >= 0) { uri = pgpath.substring(0, j + 1) + uri; } else { log.warn("The current page doesn't contain '/':" + pgpath); } } } } //locate by locale and browser if necessary uri = Servlets.locate(ctx, request, uri, null); //prefix context path if (!ctxpathSpecified && uri.charAt(0) == '/' //ZK-3131: do not prefix context path to relative protocol urls that starts with // && !(uri.length() > 1 && uri.charAt(1) == '/') && (request instanceof HttpServletRequest)) { //Work around with a bug when we wrap Pluto's RenderRequest (1.0.1) String ctxpath = ((HttpServletRequest) request).getContextPath(); if (ctxpath.length() > 0 && ctxpath.charAt(0) != '/') ctxpath = '/' + ctxpath; //Some Web server's ctxpath is "/" final int last = ctxpath.length() - 1; if (last >= 0 && ctxpath.charAt(last) == '/') ctxpath = ctxpath.substring(0, last); uri = ctxpath + uri; } int j = uri.indexOf('?'); if (j < 0) { uri = encodeURI(uri); } else { uri = encodeURI(uri.substring(0, j)) + uri.substring(j); } //encode if (response instanceof HttpServletResponse) uri = ((HttpServletResponse) response).encodeURL(uri); return uri; } /** The URL encoder used to encode URL by including the session ID, * and servlet context path. * It is a plugin that {@link Encodes#encodeURL} will call * to encode the URL. * * <p>In a sophisticated environment, e.g., * <a href="http://en.wikipedia.org/wiki/Reverse_proxy">Reverse Proxy</a>, * the encoded URL might have to be prefixed with some special prefix. * To do that, you can implement {@link URLEncoder}, and then * specify the class with the library property called * <code>org.zkoss.web.servlet.http.URLEncoder</code>. * When {@link #encodeURL} encodes an URL, it will invoke * {@link URLEncoder#encodeURL} such you can do customized encoding, * such as insert a special prefix. * * @since 3.0.1 * @see Encodes#encodeURL */ public static interface URLEncoder { /** Encodes the specified URL by including the session ID and * the servlet context path, if necessary. * * <p>Notice that url might contain "~" and other special * characters that the Web server won't support. * The implementation might invoke back the default encoding * by use of the defaultEcoder parameter. * * @param url the URL to encode. It shall not include * the servlet context path. * @param defaultEncoder the default encoder (never null). * @since 5.0.0 */ public String encodeURL(ServletContext ctx, ServletRequest request, ServletResponse response, String url, URLEncoder defaultEncoder) throws Exception; } }