From 892e44ae170e2c09c67b0d3cae2ce3591cf156e3 Mon Sep 17 00:00:00 2001 From: Heng Sin Low Date: Mon, 13 Sep 2010 23:43:47 +0800 Subject: [PATCH] better IE8 support. --- .../src/org/adempiere/webui/apps/AEnv.java | 7 +- .../src/org/zkoss/web/servlet/Servlets.java | 1228 +++++++++++++++++ zkwebui/index.zul | 1 + 3 files changed, 1231 insertions(+), 5 deletions(-) create mode 100644 zkwebui/WEB-INF/src/org/zkoss/web/servlet/Servlets.java diff --git a/zkwebui/WEB-INF/src/org/adempiere/webui/apps/AEnv.java b/zkwebui/WEB-INF/src/org/adempiere/webui/apps/AEnv.java index 0612f70ec3..8e36b03163 100644 --- a/zkwebui/WEB-INF/src/org/adempiere/webui/apps/AEnv.java +++ b/zkwebui/WEB-INF/src/org/adempiere/webui/apps/AEnv.java @@ -565,7 +565,7 @@ public final class AEnv } /** - * @return true if user agent is internet explorer + * @return true if user agent is internet explorer 7 or lower */ public static boolean isInternetExplorer() { @@ -575,10 +575,7 @@ public final class AEnv Object n = execution.getNativeRequest(); if (n instanceof ServletRequest) { - String userAgent = Servlets.getUserAgent((ServletRequest) n); - if (userAgent.indexOf("MSIE ") >= 0) { - return true; - } + return Servlets.isExplorer((ServletRequest) n); } return false; } diff --git a/zkwebui/WEB-INF/src/org/zkoss/web/servlet/Servlets.java b/zkwebui/WEB-INF/src/org/zkoss/web/servlet/Servlets.java new file mode 100644 index 0000000000..0b1af6bb3f --- /dev/null +++ b/zkwebui/WEB-INF/src/org/zkoss/web/servlet/Servlets.java @@ -0,0 +1,1228 @@ +/* Servlets.java + +{{IS_NOTE + Purpose: + Description: + History: + 90/12/10 22:24:28, Create, Tom M. Yeh. +}}IS_NOTE + +Copyright (C) 2001 Potix Corporation. All Rights Reserved. + +{{IS_RIGHT + This program is distributed under GPL Version 3.0 in the hope that + it will be useful, but WITHOUT ANY WARRANTY. +}}IS_RIGHT +*/ +package org.zkoss.web.servlet; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.RequestDispatcher; +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.HttpSession; + +import org.zkoss.idom.Element; +import org.zkoss.idom.input.SAXBuilder; +import org.zkoss.lang.SystemException; +import org.zkoss.util.CacheMap; +import org.zkoss.util.Checksums; +import org.zkoss.util.Locales; +import org.zkoss.util.resource.Locator; +import org.zkoss.util.resource.Locators; +import org.zkoss.web.Attributes; +import org.zkoss.web.servlet.http.Encodes; +import org.zkoss.web.util.resource.ExtendletContext; +import org.zkoss.web.util.resource.ServletContextLocator; + +/** + * The servlet relevant utilities. + * + * @author tomyeh + * @see org.zkoss.web.servlet.http.Https + * @see org.zkoss.web.servlet.Charsets + */ +public class Servlets { +// private static final Log log = Log.lookup(Servlets.class); + + private static final boolean _svl24, _svl23; + static { + boolean b = false; + try { + ServletResponse.class.getMethod("getContentType", new Class[0]); + b = true; + } catch (Throwable ex) { + } + _svl24 = b; + + if (!b) { + try { + HttpSession.class.getMethod("getServletContext", new Class[0]); + b = true; + } catch (Throwable ex) { + } + } + _svl23 = b; + } + + /** Utilities; no instantiation required. */ + protected Servlets() {} + + /** Returns whether a URL starts with xxx://, mailto:, about:, + * javascript: + */ + public static final boolean isUniversalURL(String uri) { + if (uri == null || uri.length() == 0) return false; + + final char cc = uri.charAt(0); + return cc >= 'a' && cc <= 'z' + && (uri.indexOf("://") > 0 || uri.startsWith("mailto:") + || uri.startsWith("javascript:") || uri.startsWith("about:")); + } + + /** Returns whether the current Web server supports Servlet 2.4 or above. + * + * @since 3.0.0 + */ + public static final boolean isServlet24() { + return _svl24; + } + /** Returns whether the current Web server supports Servlet 2.3 or above. + * Thus, if {@link #isServlet24} returns true, {@link #isServlet23} + * must return true, too. + * + * @since 3.0.0 + */ + public static final boolean isServlet23() { + return _svl23; + } + + //-- resource locator --// + /** Locates a page based on the specified Locale. It never returns null. + * + *

Notice that it cannot resolve a path starting with '~', and containing + * '*', because it cannot access the content of the other servlet context. + * + *

If an URI contains "*", it will be replaced with a proper Locale. + * For example, if the current Locale is zh_TW and the resource is + * named "ab*.cd", then it searches "ab_zh_TW.cd", "ab_zh.cd" and + * then "ab.cd", until any of them is found. + * + *

Note: "*" must be right before ".", or the last character. + * For example, "ab*.cd" and "ab*" are both correct, while + * "ab*cd" and "ab*\/cd" are ignored.
+ * + *

If an URI contains two "*", the first "*" will be replaced with + * a browser code and the second with a proper locale. + * The browser code depends on what browser + * the user are used to visit the web site. + * Currently, the code for Internet Explorer is "ie", Safari is "saf", + * Opera is "opr" and all others are "moz". + * Thus, in the above example, if the resource is named "ab**.cd" + * and Firefox is used, then it searches "abmoz_zh_TW.cd", "abmoz_zh.cd" + * and then "abmoz.cd", until any of them is found. + * + *

Note: it assumes the path as name_lang_cn_var.ext where + * ".ext" is optional. Example, my_zh_tw.html.jsp. + * + * @param ctx the servlet context to locate pages + * @param pgpath the page path excluding servlet name. It is OK to have + * the query string. It might contain "*" for current browser code and Locale. + * @param locator the locator used to locate resource. If null, ctx + * is assumed. + * @return the path that matches the wildcard; pgpath, otherwise + * never null + */ + public static final String locate(ServletContext ctx, + ServletRequest request, String pgpath, Locator locator) + throws ServletException { + if (pgpath == null) + return pgpath; + final int f = pgpath.indexOf('*'); + if (f < 0 || isUniversalURL(pgpath)) + return pgpath; + final int jquest = pgpath.indexOf('?'); + if (jquest >= 0 && f > jquest) + return pgpath; + //optimize the case that no "*" at all + + final String qstr; + if (jquest >= 0) { + qstr = pgpath.substring(jquest); + pgpath = pgpath.substring(0, jquest); + } else { + qstr = null; + } + + //by browser? + int l = pgpath.lastIndexOf('*'); + if (l > f) { //two '*' + final String bc = Servlets.isExplorer(request) ? "ie": + Servlets.isSafari(request) ? "saf": + Servlets.isOpera(request) ? "opr": "moz"; + l += bc.length() - 1; + pgpath = pgpath.substring(0, f) + bc + pgpath.substring(f + 1); + } + + //remove "*" + pgpath = pgpath.substring(0, l) + pgpath.substring(l + 1); //remove + + //by locale? 1) before the first dot, 2) the last char if no dot + boolean byLocale = l == pgpath.length() + || (pgpath.charAt(l) == '.' && pgpath.indexOf('/', l + 1) < 0); + if (byLocale) { + //make sure no dot before it + for (int j = l; --j >= 0;) { + final char cc = pgpath.charAt(j); + if (cc == '.') { + byLocale = false; + break; + } else if (cc == '/') { + break; + } + } + } + if (!byLocale) + return qstr != null ? pgpath + qstr: pgpath; //not by locale + + + final String PGPATH_CACHE = "s_pgpath_cache"; + Map map = (Map)ctx.getAttribute(PGPATH_CACHE); + if (map == null) { + map = Collections.synchronizedMap( //10 min + new CacheMap(500, 10*60*1000)); + ctx.setAttribute(PGPATH_CACHE, map); + } + + final Locale locale = Locales.getCurrent(); + final URIIndex index = new URIIndex(pgpath, locale); + + String uri = (String)map.get(index); + if (uri == null) { + final Locators.URLLocation loc = + Locators.locate(pgpath, locale, + locator != null ? locator: new ServletContextLocator(ctx)); + uri = loc != null ? loc.file: pgpath; + map.put(index, uri); + } + + return qstr != null ? uri + qstr: uri; + } + private static class URIIndex { + private final String _uri; + private final Locale _locale; + private URIIndex(String uri, Locale locale) { + if (uri == null || locale == null) + throw new IllegalArgumentException("null"); + _uri = uri; + _locale = locale; + } + public int hashCode() { + return _uri.hashCode(); + } + public boolean equals(Object o) { + //To speed up, don't check whether o is the right class + final URIIndex idx = (URIIndex)o; + return _uri.equals(idx._uri) && _locale.equals(idx._locale); + } + } + + /** Returns whether the client is a browser of the specified type. + * + * @param type the type of the browser. + * Allowed values include "robot", "ie", "ie6", "ie6-", "ie7", "ie8", "ie8-", + * "ie7-", "gecko", "gecko2", "gecko3", "gecko3.5", "gecko2-", "gecko3-", + * "opara", "safari", + * "mil", "hil", "mil-".
+ * Note: "ie6-" means Internet Explorer 6 only; not Internet Explorer 7 + * or other. + * @since 3.5.1 + */ + public static boolean isBrowser(ServletRequest req, String type) { + return (req instanceof HttpServletRequest) + && isBrowser(((HttpServletRequest)req).getHeader("user-agent"), type); + } + /** Returns whether the user agent is a browser of the specified type. + * + * @param type the type of the browser. + * Allowed values include "robot", "ie", "ie6", "ie6-", "ie7", "ie8", + * "ie7-", "gecko", "gecko2", "gecko3", "gecko3.5", "gecko2-", "gecko3-", + * "opara", "safari", + * "mil", "hil", "mil-". Otherwise, it matches whether the type exist or not.
+ * Note: "ie6-" means Internet Explorer 6 only; not Internet Explorer 7 + * or other. + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static boolean isBrowser(String userAgent, String type) { + if ("ie".equals(type) || "ie6".equals(type)) return isExplorer(userAgent); + if ("ie6-".equals(type)) return getIEVer(userAgent) == 6; + if ("ie7".equals(type)) return isExplorer7(userAgent); + if ("ie7-".equals(type)) return getIEVer(userAgent) == 7; + if ("ie8".equals(type)) return getIEVer(userAgent) >= 8; + if ("ie8-".equals(type)) return getIEVer(userAgent) == 8; + + if ("gecko".equals(type) || "gecko2".equals(type)) return isGecko(userAgent); + if ("gecko2-".equals(type)) return getGeckoVer(userAgent) == 2; + if ("gecko3".equals(type)) return isGecko3(userAgent); + if ("gecko3.5".equals(type)) return getGeckoVer(userAgent, true) >= 35; + if ("gecko3-".equals(type)) return getGeckoVer(userAgent) == 3; + + if ("safari".equals(type)) return isSafari(userAgent); + if ("opera".equals(type)) return isOpera(userAgent); + + if ("mil".equals(type)) return isMilDevice(userAgent); + if ("mil-".equals(type)) return isMilDevice(userAgent) && !isHilDevice(userAgent); + if ("hil".equals(type)) return isHilDevice(userAgent); + + if ("robot".equals(type)) return isRobot(userAgent); + return userAgent != null && type != null && userAgent.toLowerCase().indexOf(type.toLowerCase()) > -1; + } + /** Returns whether the client is a robot (such as Web crawlers). + * + *

Because there are too many robots, it returns true if the user-agent + * is not recognized. + */ + public static final boolean isRobot(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isRobot(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the client is a robot (such as Web crawlers). + * + *

Because there are too many robots, it returns true if the user-agent + * is not recognized. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isRobot(String userAgent) { + if (userAgent == null) + return false; + + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("msie ") < 0 && userAgent.indexOf("opera") < 0 + && userAgent.indexOf("gecko/") < 0 && userAgent.indexOf("safari") < 0 + && userAgent.indexOf("zk") < 0 && userAgent.indexOf("rmil") < 0; + } + /** Returns whether the browser is Internet Explorer. + * If true, it also implies {@link #isExplorer7} is true. + */ + public static final boolean isExplorer(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isExplorer(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Internet Explorer. + * If true, it also implies {@link #isExplorer7} is true. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isExplorer(String userAgent) { + if (userAgent == null) + return false; + + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("msie ") >= 0 && userAgent.indexOf("opera") < 0 && userAgent.indexOf("chromeframe") < 0 + && getIEVer(userAgent) <= 7; + } + /** Returns whether the browser is Explorer 7 or later. + */ + public static final boolean isExplorer7(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isExplorer7(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Explorer 7 or later. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isExplorer7(String userAgent) { + return getIEVer(userAgent) == 7; + } + private static final int getIEVer(String userAgent) { + if (userAgent == null) return -1; +/* + * IE8 on Windows Vista (Compatibility View) + * Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0) + * IE8 on Windows Vista + * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0) + * IE8 on Windows 7 + * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0) + * 64-bit IE on 64-bit Windows: + * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Win64; x64; Trident/4.0) + * 32-bit IE on 64-bit Windows: + * Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; Trident/4.0) + * + */ + userAgent = userAgent.toLowerCase(); + int j = userAgent.indexOf("msie "); + if (j < 0 || userAgent.indexOf("opera") >= 0) return -1; + + return parseVer(userAgent, j + 5)[0]; + } + private static final int[] parseVer(String ua, int j) { + int ver = 0; + for (int len = ua.length(); j < len; ++j) { + final char cc = ua.charAt(j); + if (cc >= '0' && cc <= '9') + ver = ver * 10 + cc - '0'; + else + break; + } + return new int[] {ver, j}; + } + + /** Returns whether the browser is Gecko based, such as Mozilla, Firefox and Camino + * If true, it also implies {@link #isGecko3} is true. + */ + public static final boolean isGecko(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isGecko(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Gecko based, such as Mozilla, Firefox and Camino + * If true, it also implies {@link #isGecko3} is true. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isGecko(String userAgent) { + if (userAgent == null) + return false; + + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("gecko/") >= 0 && userAgent.indexOf("safari") < 0; + } + /** Returns whether the browser is Gecko 3 based, such as Firefox 3. + * @since 3.5.0 + */ + public static final boolean isGecko3(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isGecko3(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Gecko 3 based, such as Firefox 3. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isGecko3(String userAgent) { + return getGeckoVer(userAgent) >= 3; + } + private static final int getGeckoVer(String userAgent) { + return getGeckoVer(userAgent, false); + } + /** + * @param subversion whether to include the subversion. + * If true, it returns 30 instead of 3. It is useful to detect FF 3.5 + * (which will return 35 if subversion is true). + */ + private static final int getGeckoVer(String userAgent, boolean subversion) { + if (userAgent == null) return -1; + /* + Firefox 3.0.8 + Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 (.NET CLR 3.5.30729) FirePHP/0.2.4 + Firefox 2.0.0.20 + Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) + Safari 4 beta + Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 + Opera 9.64 + Opera/9.64 (Windows NT 5.1; U; en) Presto/2.1.1 + */ + userAgent = userAgent.toLowerCase(); + if (userAgent.indexOf("gecko/") < 0 || userAgent.indexOf("safari") >= 0 + || userAgent.indexOf("opera") >= 0) + return -1; + + int j = userAgent.indexOf("firefox/"); + if (j < 0) return -1; + + int[] vi = parseVer(userAgent, j + 8); + int ver = vi[0]; + if (subversion) + ver = ver * 10 + parseVer(userAgent, vi[1] + 1)[0]; + return ver; + } + + /** Returns whether the browser is Safari. + */ + public static final boolean isSafari(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isSafari(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Safari. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isSafari(String userAgent) { + if (userAgent == null) + return false; + + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("safari") >= 0; + } + + /** Returns whether the browser is Opera. + */ + public static final boolean isOpera(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isOpera(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the browser is Opera. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isOpera(String userAgent) { + if (userAgent == null) + return false; + + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("opera") >= 0; + } + + /** Returns whether the client is a mobile device supporting MIL + * (Mobile Interactive Language). + * @since 2.4.0 + */ + public static final boolean isMilDevice(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isMilDevice(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the client is a mobile device supporting MIL + * (Mobile Interactive Language). + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isMilDevice(String userAgent) { + if (userAgent == null) + return false; + + //ZK Mobile/1.0 (RMIL) + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("zk") >= 0 && userAgent.indexOf("rmil") >= 0; + } + /** Returns whether the client is a mobile device supporting HIL + * (Handset Interactive Language). + * + *

Note: ZK Mobile for Android supports both MIL and HIL. + * That is, both {@link #isHilDevice} and {@link #isMilDevice} + * return true. + * + * @since 3.0.2 + */ + public static final boolean isHilDevice(ServletRequest req) { + return (req instanceof HttpServletRequest) + && isHilDevice(((HttpServletRequest)req).getHeader("user-agent")); + } + /** Returns whether the client is a mobile device supporting HIL + * (Handset Interactive Language). + * + *

Note: ZK Mobile for Android supports both MIL and HIL. + * That is, both {@link #isHilDevice} and {@link #isMilDevice} + * return true. + * + * @param userAgent represents a client. + * For HTTP clients, It is the user-agent header. + * @since 3.5.1 + */ + public static final boolean isHilDevice(String userAgent) { + if (userAgent == null) + return false; + + //ZK Mobile for Android 1.0 (RMIL; RHIL) + userAgent = userAgent.toLowerCase(); + return userAgent.indexOf("zk") >= 0 && userAgent.indexOf("rhil") >= 0; + } + + /** Returns the user-agent header, which indicates what the client is, + * or an empty string if not available. + * + *

Note: it doesn't return null, so it is easy to test what + * the client is with {@link String#indexOf}. + * + * @since 3.0.2 + */ + public static final String getUserAgent(ServletRequest req) { + if (req instanceof HttpServletRequest) { + final String s = ((HttpServletRequest)req).getHeader("user-agent"); + if (s != null) return s; + } + return ""; + } + + /** + * Tests whether this page is included by another page. + */ + public static final boolean isIncluded(ServletRequest request) { + return request.getAttribute(Attributes.INCLUDE_CONTEXT_PATH) != null + || request.getAttribute("org.zkoss.web.servlet.include") != null; + //org.zkoss.web.servlet.include is used by ZK (or others) + //to 'simulate' inclusion + } + /** + * Tests whether this page is forwarded by another page. + */ + public static final boolean isForwarded(ServletRequest request) { + return request.getAttribute(Attributes.FORWARD_CONTEXT_PATH) != null + || request.getAttribute("org.zkoss.web.servlet.forward") != null; + } + /** + * Forward to the specified URI. + * It enhances RequestDispatcher in the following ways. + * + *

+ * + *

NOTE: don't include query parameters in uri. + * + * @param ctx the servlet context. If null, uri cannot be foreign URI. + * It is ignored if URI is relevant (neither starts with '/' nor '~'). + * @param uri the URI to include. It is OK to relevant (without leading + * '/'). If starts with "/", the context path of request is assumed. + * To reference to foreign context, use "~ctx" where ctx is the + * context path of the foreign context (without leading '/'). + * If it could be any context path recognized by the Web container or + * any name registered with {@link #addExtendletContext}. + *
Notice that, since 3.6.3, uri could contain + * '*' (to denote locale and browser). Refer to {@link #locate}. + * @param params the parameter map; null to ignore + * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM}, + * and {@link #APPEND_PARAM}. It defines how to handle if both uri + * and params contains the same parameter. + */ + public static final + void forward(ServletContext ctx, ServletRequest request, + ServletResponse response, String uri, Map params, int mode) + throws IOException, ServletException { +// if (D.ON && log.debugable()) log.debug("Forwarding "+uri); + + //include or foward depending whether this page is included or not + if (isIncluded(request)) { + include(ctx, request, response, uri, params, mode); + return; + } + + uri = locate(ctx, request, uri, null); + + final RequestDispatcher disp = + getRequestDispatcher(ctx, request, uri, params, mode); + if (disp == null) + throw new ServletException("No dispatcher available to forward to "+uri); + + if (mode == PASS_THRU_ATTR && params != null && !params.isEmpty()) { + final Map old = setPassThruAttr(request, params); + try { + disp.forward(request, response); + } catch (ClassCastException ex) { + //Tom M. Yeh, 2006/09/21: Bug 1548478 + //Cause: http://issues.apache.org/bugzilla/show_bug.cgi?id=39417 + // + //Bug or limitation of Catalina: not accepting HttpServletRequest + //othere than the original one or wrapper of original one + // + //Real Cause: org.apache.catalina.core.ApplicationDispatcher + //call unwrapRequest() twice, and then causes ClassCastException + // + //Resolution: since it is the almost last statement, it is safe + //to ignore this exception + if (!(request instanceof org.zkoss.web.portlet.RenderHttpServletRequest)) + throw ex; //not the case described above + } finally { + restorePassThruAttr(request, old); + } + } else { + disp.forward(request, response); + } + } + /** A shortcut of forward(request, response, uri, null, 0). + */ + public static final + void forward(ServletContext ctx, ServletRequest request, + ServletResponse response, String uri) throws IOException, ServletException { + forward(ctx, request, response, uri, null, 0); + } + /** + * Includes the resource at the specified URI. + * It enhances RequestDispatcher to allow the inclusion with + * a parameter map -- acutually converting parameters to a query string + * and appending it to uri. + * + *

NOTE: don't include query parameters in uri. + * + * @param ctx the servlet context. If null, uri cannot be foreign URI. + * It is ignored if URI is relevant (neither starts with '/' nor '~'). + * @param uri the URI to include. It is OK to relevant (without leading + * '/'). If starts with "/", the context path of request is assumed. + * To reference to foreign context, use "~ctx/" where ctx is the + * context path of the foreign context (without leading '/'). + *
Notice that, since 3.6.3, uri could contain + * '*' (to denote locale and browser). Refer to {@link #locate}. + * @param params the parameter map; null to ignore + * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM}, + * and {@link #APPEND_PARAM}. It defines how to handle if both uri + * and params contains the same parameter. + */ + public static final + void include(ServletContext ctx, ServletRequest request, + ServletResponse response, String uri, Map params, int mode) + throws IOException, ServletException { +// if (D.ON && log.debugable()) log.debug("Including "+uri+" at "+ctx); + + //Note: we don't optimize the include to call ClassWebResource here + //since 1) it is too low level (might have some risk) + //2) no clean way to access ClassWebResouce here + + //20050606: Tom Yeh + //We have to set this special attribute for jetty + //Otherwise, if including a page crossing context might not return + //the same session + request.setAttribute("org.mortbay.jetty.servlet.Dispatcher.shared_session", Boolean.TRUE); + + uri = locate(ctx, request, uri, null); + + final RequestDispatcher disp = + getRequestDispatcher(ctx, request, uri, params, mode); + if (disp == null) + throw new ServletException("No dispatcher available to include "+uri); + + if (mode == PASS_THRU_ATTR && params != null && !params.isEmpty()) { + final Map old = setPassThruAttr(request, params); + try { + disp.include(request, response); + } finally { + restorePassThruAttr(request, old); + } + } else { + disp.include(request, response); + } + } + /** A shortcut of include(request, response, uri, null, 0). + */ + public static final + void include(ServletContext ctx, ServletRequest request, + ServletResponse response, String uri) throws IOException, ServletException { + include(ctx, request, response, uri, null, 0); + } + /** Sets the arg attribute to pass parameters thru request's attribute. + */ + private static final + Map setPassThruAttr(ServletRequest request, Map params) { + final Map old = (Map)request.getAttribute(Attributes.ARG); + request.setAttribute(Attributes.ARG, params); + return old; + } + /** Restores what has been done by {@link #setPassThruAttr}. + */ + private static final + void restorePassThruAttr(ServletRequest request, Map old) { + if (old != null) + request.setAttribute(Attributes.ARG, old); + else + request.removeAttribute(Attributes.ARG); + } + /** Returns the request dispatch of the specified URI. + * + * @param ctx the servlet context. If null, uri cannot be foreign URI. + * It is ignored if uri is relevant (neither starts with '/' nor '~'). + * @param request the request. If null, uri cannot be relevant. + * It is used only if uri is relevant. + * @param uri the URI to include. It is OK to relevant (without leading + * '/'). If starts with "/", the context path of request is assumed. + * To reference to foreign context, use "~ctx/" where ctx is the + * context path of the foreign context (without leading '/'). + * @param params the parameter map; null to ignore + * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM}, + * and {@link #APPEND_PARAM}. It defines how to handle if both uri + * and params contains the same parameter. + */ + public static final RequestDispatcher + getRequestDispatcher(ServletContext ctx, ServletRequest request, + String uri, Map params, int mode) + throws ServletException { + final char cc = uri.length() > 0 ? uri.charAt(0): (char)0; + if (ctx == null || (cc != '/' && cc != '~')) {//... or relevant + if (request == null) + throw new IllegalArgumentException( + ctx == null ? + "Servlet context and request cannot be both null": + "Request is required to use revalant URI: "+uri); + if (cc == '~') + throw new IllegalArgumentException("Servlet context is required to use foreign URI: "+uri); + uri = generateURI(uri, params, mode); + return request.getRequestDispatcher(uri); + } + + //NO NEED to encodeURL since it is forward/include + return new ParsedURI(ctx, uri).getRequestDispatcher(params, mode); + } + /** Returns the resource of the specified uri. + * Unlike ServletContext.getResource, it handles "~" like + * {@link #getRequestDispatcher} did. + */ + public static final URL getResource(ServletContext ctx, String uri) + throws MalformedURLException { + return new ParsedURI(ctx, uri).getResource(); + } + /** Returns the resource stream of the specified uri. + * Unlike ServletContext.getResource, it handles "~" like + * {@link #getRequestDispatcher} did. + */ + public static final InputStream getResourceAsStream( + ServletContext ctx, String uri) { + return new ParsedURI(ctx, uri).getResourceAsStream(); + } + /** Used to resolve "~" in URI. */ + private static class ParsedURI { + private ServletContext _svlctx; + private ExtendletContext _extctx; + private String _uri; + + private ParsedURI(final ServletContext ctx, final String uri) { + if (uri != null && uri.startsWith("~")) { //refer to foreign context + final int j = uri.indexOf('/', 1); + final String ctxroot; + if (j >= 0) { + ctxroot = "/" + uri.substring(1, j); + _uri = uri.substring(j); + } else { + ctxroot = "/" + uri.substring(1); + _uri = "/"; + } + + _extctx = getExtendletContext(ctx, ctxroot.substring(1)); + if (_extctx == null) { + _svlctx = ctx.getContext(ctxroot); + if (_svlctx == null) + throw new SystemException("Context not found or not visible to "+ctx+": "+ctxroot); + } + } else { + _svlctx = ctx; + _uri = uri; + } + } + private RequestDispatcher getRequestDispatcher(Map params, int mode) { + if (_extctx == null && _svlctx == null) //not found + return null; + + final String uri = generateURI(_uri, params, mode); + return _svlctx != null ? _svlctx.getRequestDispatcher(uri): + _extctx.getRequestDispatcher(uri); + } + private URL getResource() throws MalformedURLException { + return _svlctx != null ? _svlctx.getResource(_uri): + _extctx != null ? _extctx.getResource(_uri): null; + } + private InputStream getResourceAsStream() { + return _svlctx != null ? _svlctx.getResourceAsStream(_uri): + _extctx != null ? _extctx.getResourceAsStream(_uri): null; + } + } + + /** Whether to overwrite uri if both uri and params contain the same + * parameter. + * Used by {@link #generateURI} + */ + public static final int OVERWRITE_URI = 0; + /** Whether to ignore params if both uri and params contain the same + * parameter. + * Used by {@link #generateURI} + */ + public static final int IGNORE_PARAM = 1; + /** Whether to append params if both uri and params contain the same + * parameter. In other words, they both appear as the final query string. + * Used by {@link #generateURI} + */ + public static final int APPEND_PARAM = 2; + /** Whether the specified parameters shall be passed thru the request + * attribute called arg. + */ + public static final int PASS_THRU_ATTR = 3; + /** Generates URI by appending the parameters. + * Note: it doesn't support the ~xxx/ format. + * + * @param params the parameters to apend to the query string + * @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM}, + * {@link #APPEND_PARAM} and {@link #PASS_THRU_ATTR}. + * It defines how to handle if both uri and params contains the same + * parameter. + * mode is used only if both uri contains query string and params is + * not empty. + * @see Encodes#encodeURL(ServletContext, ServletRequest, ServletResponse, String) + */ + public static final String generateURI(String uri, Map params, int mode) { + if (uri.startsWith("~")) + throw new IllegalArgumentException("~ctx not supported here: "+uri); + + final int j = uri.indexOf('?'); + String qstr = null; + if (j >= 0) { + qstr = uri.substring(j); + uri = uri.substring(0, j); + } + + //if (D.ON && uri.indexOf('%') >= 0) + // log.warning(new IllegalStateException("You might encode URL twice: "+uri)); + //might too annoying + + try { + uri = Encodes.encodeURI(uri); + final boolean noQstr = qstr == null; + final boolean noParams = + mode == PASS_THRU_ATTR || params == null || params.isEmpty(); + if (noQstr && noParams) + return uri; + + if (noQstr != noParams) + mode = APPEND_PARAM; + + final StringBuffer sb = new StringBuffer(80).append(uri); + if (qstr != null) sb.append(qstr); + + switch (mode) { + case IGNORE_PARAM: + //removing params that is conflict + for (final Iterator it = params.entrySet().iterator(); + it.hasNext();) { + final Map.Entry me = (Map.Entry)it.next(); + final String nm = (String)me.getKey(); + if (Encodes.containsQuery(qstr, nm)) + it.remove(); + } + //flow thru + case OVERWRITE_URI: + return Encodes.setToQueryString(sb, params).toString(); + case APPEND_PARAM: + return Encodes.addToQueryString(sb, params).toString(); + default: + throw new IllegalArgumentException("Unknown mode: "+mode); + } + } catch (UnsupportedEncodingException ex) { + throw new SystemException(ex); + } + } + + /** A list of context root paths (e.g., "/abc"). */ + private static List _ctxroots; + /** Returns a list of context paths (e.g., "/abc") that this application + * has. This implementation parse application.xml. For war that doesn't + * contain application.xml might have to override this method and + * parse another file to know what context being loaded. + */ + public static final List getContextPaths() { + if (_ctxroots != null) + return _ctxroots; + + try { + synchronized (Servlets.class) { + return _ctxroots = myGetContextPaths(); + } + } catch (Exception ex) { + throw SystemException.Aide.wrap(ex); + } + } + private static final List myGetContextPaths() throws Exception { + final String APP_XML = "/META-INF/application.xml"; + final List ctxroots = new LinkedList(); + final URL xmlURL = Locators.getDefault().getResource(APP_XML); + if (xmlURL == null) + throw new SystemException("File not found: "+APP_XML); + +// if (log.debugable()) log.debug("Parsing "+APP_XML); + final Element root = + new SAXBuilder(false,false,true).build(xmlURL).getRootElement(); + + for (Iterator it = root.getElements("module").iterator(); + it.hasNext();) { + final Element e = (Element)it.next(); + final String ctxroot = (String)e.getContent("web/context-root"); + if (ctxroot == null) { +// if (D.ON && log.finerable()) log.finer("Skip non-web: "+e.getContent("java")); + continue; + } + + ctxroots.add(ctxroot.startsWith("/") ? ctxroot: "/" + ctxroot); + } + +// log.info("Context found: "+ctxroots); + return new ArrayList(ctxroots); + } + + /** Returns a token to represent a limit-time offer. + * It is mainly used as an parameter value (mostlycalled zk_lto), and then + * you could verify whether it is expired by {@link #isOfferExpired}. + */ + public static final String getLimitTimeOffer() { + final String lto = Long.toHexString(System.currentTimeMillis()); + return lto + Checksums.getChecksum(lto, ""); + } + /** Returns whether a token returned by getLimitTimeOffer expired. + * @param timeout how long the office shall expire, unit: seconds. + */ + public static final boolean isOfferExpired(String lto, int timeout) { + final int len = lto != null ? lto.length(): 0; + if (len <= 1) + return true; + + final char cksm = lto.charAt(len - 1); + lto = lto.substring(0, len - 1); + if (cksm != Checksums.getChecksum(lto, "")) + return true; + + try { + return Long.parseLong(lto, 16) + timeout*1000L + < System.currentTimeMillis(); + } catch (NumberFormatException ex) { + return true; + } + } + + /** Adds an extended context. + * @return the previous extended context, if any, associated with + * the specified name. + */ + public static final + ExtendletContext addExtendletContext(ServletContext ctx, + String name, ExtendletContext extctx) { + if (name == null || extctx == null) + throw new IllegalArgumentException("null"); + return (ExtendletContext)getExtWebCtxs(ctx).put(name, extctx); + } + /** Removes an extended context of the specified name. + */ + public static final + ExtendletContext removeExtendletContext(ServletContext ctx, String name) { + return (ExtendletContext)getExtWebCtxs(ctx).remove(name); + } + /** Returns the extended context of the specified name. + */ + public static final + ExtendletContext getExtendletContext(ServletContext ctx, String name) { + return (ExtendletContext)getExtWebCtxs(ctx).get(name); + } + private static final Map getExtWebCtxs(ServletContext ctx) { + final String attr = "javax.zkoss.web.servlets.ExtendletContexts"; + //such that it could be shared among portlets + synchronized (Servlets.class) { //don't use ctx because it might be a proxy (in portlet) + Map ctxs = (Map)ctx.getAttribute(attr); + if (ctxs == null) + ctx.setAttribute(attr, + ctxs = Collections.synchronizedMap(new HashMap(5))); + return ctxs; + } + } + + /** Returns the file/path extension of the specified path (excluding dot), + * or null if no extension at all. + * + *

Note: the extension is converted to the lower case. + * + * @param path the path. If path is null, null is returned. + * @since 2.4.1 + * @see #getExtension(String, boolean) + */ + public static final String getExtension(String path) { + if (path != null) { + int j = path.lastIndexOf('.'); + if (j >= 0 && path.indexOf('/', j + 1) < 0) + return path.substring(j + 1).toLowerCase(); + //don't worry jsessionid since it is handled by container + } + return null; + } + /** Returns the file/path extension of the specified path (excluding dot), + * or null if no extension at all. + * + *

Note: the extension is converted to the lower case. + * + *

The result is the same for both {@link #getExtension(String)} + * and {@link #getExtension(String, boolean)}, if the path + * has only one dot. However, if there are more than one dot, e.g., + * /a/b.c.d, then {@link #getExtension(String)} retrieves the last + * extension, that is, d in this example. + * On the other hand, if you invoke getExtension(path, false), + * it returns the complete extension, that is, c.d in this example. + * + * @param path the path. If path is null, null is returned. + * @param lastOnly whether to return the last portion of extensioin + * if there are two or more dots. + * In other wors, getExtension(path) is the same as + * getExtension(path, true). + * @since 3.5.1 + */ + public static final String getExtension(String path, boolean lastOnly) { + if (lastOnly) + return getExtension(path); + if (path == null) + return null; + + int dot = -1; + for (int j = path.length(); --j >= 0;) { + final char cc = path.charAt(j); + if (cc == '.') + dot = j; + else if (cc == '/') + break; + } + return dot >= 0 ? path.substring(dot + 1).toLowerCase(): ""; + } + + /** Returns the request detail infomation. + * It is used to log the debug info. + * @since 3.0.5 + */ + public static String getDetail(ServletRequest request) { + final HttpServletRequest hreq = + request instanceof HttpServletRequest ? (HttpServletRequest)request: null; + final StringBuffer sb = new StringBuffer(128); + if (hreq != null) { + sb.append(" sid: ").append(hreq.getHeader("ZK-SID")).append('\n'); + addHeaderInfo(sb, hreq, "user-agent"); + addHeaderInfo(sb, hreq, "content-length"); + addHeaderInfo(sb, hreq, "content-type"); +// sb.append(" method: ").append(hreq.getMethod()); + } + sb.append(" ip: ").append(request.getRemoteAddr()); + return sb.toString(); + } + private static void addHeaderInfo(StringBuffer sb, + HttpServletRequest request, String header) { + sb.append(' ') + .append(header).append(": ").append(request.getHeader(header)) + .append('\n'); + } + + /** Returns the normal path; that is, will elminate the double dots + * ".."(parent) and single dot "."(current) in the path as possible. e.g. + * /abc/../def would be normalized to /def; /abc/./def would be + * normalized to /abc/def; /abc//def would be normalized to /abc/def. + *

Note that if found no way to navigate the path, it is deemed as an illegal path. e.g. + * /../abc or /../../abc is deemed as illegal path since we don't + * know how to continue doing the normalize. + * @since 3.6.2 + */ + public static String getNormalPath(String path) { + final int sz = path.length(); + final StringBuffer sb = new StringBuffer(path); + final IntStack slashes = new IntStack(32); //most 32 slash in a path + slashes.push(-1); + int j = 0, colon = -100, dot1 = -100, dot2 = -100; + for (; j < sb.length(); ++j) { + final char c = sb.charAt(j); + switch(c) { + case '/': + if (dot1 >= 0) { //single dot or double dots + if (dot2 >= 0) { //double dots + int preslash = slashes.pop(); + if (preslash == 0) { //special case "/../" + throw new IllegalArgumentException("Illegal path: "+path); + } + if (slashes.isEmpty()) { + slashes.push(-1); + } + dot2 = -100; + } + int b = slashes.peek(); + sb.delete(b + 1, j+1); + j = b; + dot1 = -100; + } else { //no dot + int s = slashes.peek(); + if (s >= 0) { + if (j == (s+1)) { //consequtive slashs + if (colon == (s-1)) { //e.g. "http://abc" + slashes.clear(); + slashes.push(-1); + slashes.push(j); + } else { + --j; + sb.delete(j, j+1); + } + continue; + } + } + slashes.push(j); + } + break; + case '.': + if (dot1 < 0) { + if (slashes.peek() == (j-1)) + dot1 = j; + } else if (dot2 < 0){ + dot2 = j; + } else { //more than 2 consecutive dots + throw new IllegalArgumentException("Illegal path: "+path); + } + break; + case ':': + if (colon >= 0) { + throw new IllegalArgumentException("Illegal path: "+path); + } + colon = j; + default: + dot1 = dot2 = -100; + } + } + return sb.toString(); + } + + private static class IntStack { + private int _top = -1; + private int[] _value; + + public IntStack(int sz) { + _value = new int[sz]; + } + public boolean isEmpty() { + return _top < 0; + } + public int peek() { + return _top >= 0 && _top < _value.length ? _value[_top] : -100; + } + public int pop() { + return _value[_top--]; + } + public void push(int val) { + _value[++_top] = val; + } + public void clear() { + _top = -1; + } + } +} diff --git a/zkwebui/index.zul b/zkwebui/index.zul index 325881cf2d..78c104caf7 100644 --- a/zkwebui/index.zul +++ b/zkwebui/index.zul @@ -4,6 +4,7 @@ Copyright (C) 2007 Ashley G Ramdass. --> +