Java tutorial
/********************************************************************************** * $URL$ * $Id$ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.portal.charon.handlers; import java.io.IOException; import java.util.List; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.regex.Pattern; import java.util.regex.Matcher; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.thread_local.cover.ThreadLocalManager; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.portal.api.Portal; import org.sakaiproject.portal.api.PortalService; import org.sakaiproject.portal.api.PortalHandlerException; import org.sakaiproject.portal.api.PortalRenderContext; import org.sakaiproject.portal.util.ByteArrayServletResponse; import org.sakaiproject.portal.util.URLUtils; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.tool.api.ActiveTool; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.api.ToolException; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.tool.cover.ActiveToolManager; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.util.Validator; import org.sakaiproject.util.Web; /** * * @author csev * @since Sakai 2.4 * @version $Rev$ * */ @SuppressWarnings("deprecation") public class PDAHandler extends SiteHandler { /** * Key in the ThreadLocalManager for access to the current http response * object. */ public final static String CURRENT_HTTP_RESPONSE = "org.sakaiproject.util.RequestFilter.http_response"; private ToolHandler toolHandler = new ToolHandler(); private static final Log log = LogFactory.getLog(PDAHandler.class); private static final String URL_FRAGMENT = "pda"; private static final String SAKAI_COOKIE_DOMAIN = "sakai.cookieDomain"; //RequestFilter.SAKAI_COOKIE_DOMAIN private static final String TOOLCONFIG_SHOW_RESET_BUTTON = "reset.button"; private static final String BYPASS_URL_PROP = "portal.pda.bypass"; private static final String DEFAULT_BYPASS_URL = "\\.jpg$|\\.gif$|\\.js$|\\.png$|\\.jpeg$|\\.prf$|\\.css$|\\.zip$|\\.pdf\\.mov$|\\.json$|\\.jsonp$\\.xml$|\\.ajax$|\\.xls$|\\.xlsx$|\\.doc$|\\.docx$|uvbview$|linktracker$|hideshowcolumns$"; // Make sure to lower-case the matching regex (i.e. don't use IResourceListener below) private static final String BYPASS_QUERY_PROP = "portal.pda.bypass.query"; private static final String DEFAULT_BYPASS_QUERY = "wicket:interface=.*iresourcelistener:|wicket:ajax=true"; private static final String BYPASS_TYPE_PROP = "portal.pda.bypass.type"; private static final String DEFAULT_BYPASS_TYPE = "^application/|^image/|^audio/|^video/|^text/xml|^text/plain"; private static final String IFRAME_SUPPRESS_PROP = "portal.pda.iframesuppress"; // SAK-22285 - says these fail in a frame // private static final String IFRAME_SUPPRESS_DEFAULT = ":all:sakai.profile2:sakai.synoptic.messagecenter:sakai.sitestats:sakai.sitestats.admin"; // SAK-25494 with the post bufffer check now working, it seems as though we can inline everything // private static final String IFRAME_SUPPRESS_DEFAULT = ":all:"; // SAK-27774 - We are going inline default but a few tools need a crutch // This is Sakai 11 only so please do not back-port or merge this private static final String IFRAME_SUPPRESS_DEFAULT = ":all:sakai.rsf.evaluation"; public PDAHandler() { setUrlFragment(PDAHandler.URL_FRAGMENT); } @Override public int doGet(String[] parts, HttpServletRequest req, HttpServletResponse res, Session session) throws PortalHandlerException { if ((parts.length == 3) && parts[1].equals(PDAHandler.URL_FRAGMENT) && parts[2].equals(XLoginHandler.URL_FRAGMENT)) { try { portal.doLogin(req, res, session, "/pda", true); return END; } catch (Exception ex) { throw new PortalHandlerException(ex); } } else if ((parts.length >= 2) && (parts[1].equals("pda"))) { // Indicate that we are the controlling portal session.setAttribute(PortalService.SAKAI_CONTROLLING_PORTAL, PDAHandler.URL_FRAGMENT); try { //check if we want to force back to the classic view String forceClassic = req.getParameter(Portal.FORCE_CLASSIC_REQ_PARAM); if (StringUtils.equals(forceClassic, "yes")) { log.debug("PDAHandler - force.classic"); //set the portal mode cookie to force classic Cookie c = new Cookie(Portal.PORTAL_MODE_COOKIE_NAME, Portal.FORCE_CLASSIC_COOKIE_VALUE); c.setPath("/"); c.setMaxAge(-1); //need to set domain and https as per RequestFilter if (System.getProperty(SAKAI_COOKIE_DOMAIN) != null) { c.setDomain(System.getProperty(SAKAI_COOKIE_DOMAIN)); } if (req.isSecure() == true) { c.setSecure(true); } res.addCookie(c); //redirect to classic view res.sendRedirect(req.getContextPath()); } // /portal/pda/site-id String siteId = null; if (parts.length >= 3) { siteId = parts[2]; } // SAK-12873 // If we have no site at all and are not logged in - and there is // only one gateway site, go directly to the gateway site if (siteId == null && session.getUserId() == null) { String siteList = ServerConfigurationService.getString("gatewaySiteList"); String gatewaySiteId = ServerConfigurationService.getGatewaySiteId(); if (siteList.trim().length() == 0 && gatewaySiteId.trim().length() != 0) { siteId = gatewaySiteId; } } // Tool resetting URL - clear state and forward to the real tool // URL // /portal/pda/site-id/tool-reset/toolId // 0 1 2 3 4 String toolId = null; if ((siteId != null) && (parts.length == 5) && (parts[3].equals("tool-reset"))) { toolId = parts[4]; String toolUrl = req.getContextPath() + "/pda/" + siteId + "/tool" + Web.makePath(parts, 4, parts.length); String queryString = Validator.generateQueryString(req); if (queryString != null) { toolUrl = toolUrl + "?" + queryString; } portalService.setResetState("true"); res.sendRedirect(toolUrl); return RESET_DONE; } // Tool after the reset // /portal/pda/site-id/tool/toolId if ((parts.length > 4) && (parts[3].equals("tool"))) { // look for page and pick up the top-left tool to show toolId = parts[4]; } String forceLogout = req.getParameter(Portal.PARAM_FORCE_LOGOUT); if ("yes".equalsIgnoreCase(forceLogout) || "true".equalsIgnoreCase(forceLogout)) { portal.doLogout(req, res, session, "/pda"); return END; } if (session.getUserId() == null) { String forceLogin = req.getParameter(Portal.PARAM_FORCE_LOGIN); if ("yes".equalsIgnoreCase(forceLogin) || "true".equalsIgnoreCase(forceLogin)) { portal.doLogin(req, res, session, URLUtils.getSafePathInfo(req), false); return END; } } SitePage page = null; // /portal/site/site-id/page/page-id // /portal/pda/site-id/page/page-id // 1 2 3 4 if ((parts.length == 5) && (parts[3].equals("page"))) { // look for page and pick up the top-left tool to show String pageId = parts[4]; page = SiteService.findPage(pageId); if (page == null) { portal.doError(req, res, session, Portal.ERROR_WORKSITE); return END; } else { List<ToolConfiguration> tools = page.getTools(0); if (tools != null && !tools.isEmpty()) { toolId = tools.get(0).getId(); } parts[3] = "tool"; parts[4] = toolId; } } // Set the site language Site site = null; if (siteId == null && session.getUserId() != null) { site = portal.getSiteHelper().getMyWorkspace(session); } else { try { Set<SecurityAdvisor> advisors = (Set<SecurityAdvisor>) session .getAttribute("sitevisit.security.advisor"); if (advisors != null) { for (SecurityAdvisor advisor : advisors) { SecurityService.pushAdvisor(advisor); } } // This should understand aliases as well as IDs site = portal.getSiteHelper().getSiteVisit(siteId); } catch (IdUnusedException e) { } catch (PermissionException e) { } } if (site != null) { super.setSiteLanguage(site); } // See if we can buffer the content, if not, pass the request through boolean allowBuffer = false; ToolConfiguration siteTool = SiteService.findTool(toolId); String commonToolId = null; String toolContextPath = null; String toolPathInfo = null; if (parts.length >= 5) { toolContextPath = req.getContextPath() + req.getServletPath() + Web.makePath(parts, 1, 5); toolPathInfo = Web.makePath(parts, 5, parts.length); } Object BC = null; if (siteTool != null && parts.length >= 5) { commonToolId = siteTool.getToolId(); // Does the tool allow us to buffer? allowBuffer = allowBufferContent(req, site, siteTool); if (allowBuffer) { // Should we bypass buffering based on the request? boolean matched = checkBufferBypass(req, siteTool); if (matched) { ActiveTool tool = ActiveToolManager.getActiveTool(commonToolId); portal.forwardTool(tool, req, res, siteTool, siteTool.getSkin(), toolContextPath, toolPathInfo); return END; } // Inform includeTool called by portal.includePortal below ThreadLocalManager.set("sakai:inline-tool", "true"); } } // Prepare for the full output... PortalRenderContext rcontext = portal.includePortal(req, res, session, siteId, toolId, req.getContextPath() + req.getServletPath(), "pda", /* doPages */false, /* resetTools */true, /* includeSummary */false, /* expandSite */false); if (allowBuffer) { BC = bufferContent(req, res, session, toolId, toolContextPath, toolPathInfo, siteTool); // If the buffered response was not parseable if (BC instanceof ByteArrayServletResponse) { ByteArrayServletResponse bufferResponse = (ByteArrayServletResponse) BC; StringBuffer queryUrl = req.getRequestURL(); String queryString = req.getQueryString(); if (queryString != null) queryUrl.append('?').append(queryString); // SAK-25494 - This probably should be a log.debug later String msg = "Post buffer bypass CTI=" + commonToolId + " URL=" + queryUrl; String redir = bufferResponse.getRedirect(); if (redir != null) msg = msg + " redirect to=" + redir; log.warn(msg); bufferResponse.forwardResponse(); return END; } } // TODO: Should this be a property? Probably because it does cause an // uncached SQL query portal.includeSubSites(rcontext, req, session, siteId, req.getContextPath() + req.getServletPath(), "pda", /* resetTools */ true); // Add the buttons if (siteTool != null) { boolean showResetButton = !"false" .equals(siteTool.getConfig().getProperty(TOOLCONFIG_SHOW_RESET_BUTTON)); rcontext.put("showResetButton", Boolean.valueOf(showResetButton)); if (toolContextPath != null && showResetButton) { rcontext.put("resetActionUrl", toolContextPath.replace("/tool/", "/tool-reset/")); } } // Include the buffered content if we have it if (BC instanceof Map) { rcontext.put("bufferedResponse", Boolean.TRUE); Map<String, String> bufferMap = (Map<String, String>) BC; rcontext.put("responseHead", (String) bufferMap.get("responseHead")); rcontext.put("responseBody", (String) bufferMap.get("responseBody")); } // Add any device specific information to the context portal.setupMobileDevice(req, rcontext); addLocale(rcontext, site); portal.sendResponse(rcontext, res, "pda", null); try { boolean presenceEvents = ServerConfigurationService.getBoolean("presence.events.log", true); if (presenceEvents) org.sakaiproject.presence.cover.PresenceService.setPresence(siteId + "-presence"); } catch (Exception e) { return END; } return END; } catch (Exception ex) { throw new PortalHandlerException(ex); } } else { return NEXT; } } /* * Check to see if this request should bypass buffering */ public boolean checkBufferBypass(HttpServletRequest req, ToolConfiguration siteTool) { String uri = req.getRequestURI(); String commonToolId = siteTool.getToolId(); boolean matched = false; // Check the URL for a pattern match String pattern = null; Pattern p = null; Matcher m = null; pattern = ServerConfigurationService.getString(BYPASS_URL_PROP, DEFAULT_BYPASS_URL); pattern = ServerConfigurationService.getString(BYPASS_URL_PROP + "." + commonToolId, pattern); if (pattern.length() > 1) { p = Pattern.compile(pattern); m = p.matcher(uri.toLowerCase()); if (m.find()) { matched = true; } } // Check the query string for a pattern match pattern = ServerConfigurationService.getString(BYPASS_QUERY_PROP, DEFAULT_BYPASS_QUERY); pattern = ServerConfigurationService.getString(BYPASS_QUERY_PROP + "." + commonToolId, pattern); String queryString = req.getQueryString(); if (queryString == null) queryString = ""; if (pattern.length() > 1) { p = Pattern.compile(pattern); m = p.matcher(queryString.toLowerCase()); if (m.find()) { matched = true; } } // wicket-ajax request can not be buffered (PRFL-405) if (Boolean.valueOf(req.getHeader("wicket-ajax"))) { matched = true; } return matched; } /* * Check to see if this tool allows the buffering of content */ public boolean allowBufferContent(HttpServletRequest req, Site site, ToolConfiguration siteTool) { String tidAllow = ServerConfigurationService.getString(IFRAME_SUPPRESS_PROP, IFRAME_SUPPRESS_DEFAULT); if (tidAllow.indexOf(":none:") >= 0) return false; // JSR-168 portlets do not operate in iframes if (portal.isPortletPlacement(siteTool)) return false; // If the property is set and :all: is not specified, then the // tools in the list are the ones that we accept if (tidAllow.trim().length() > 0 && tidAllow.indexOf(":all:") < 0) { if (tidAllow.indexOf(siteTool.getToolId()) < 0) return false; } // If the property is set and :all: is specified, then the // tools in the list are the ones that we render the old way if (tidAllow.indexOf(":all:") >= 0) { if (tidAllow.indexOf(siteTool.getToolId()) >= 0) return false; } // Need to make sure the user is allowed to visit this tool ToolManager toolManager = (ToolManager) ComponentManager.get(ToolManager.class.getName()); boolean allowedUser = toolManager.allowTool(site, siteTool); if (!allowedUser) return false; return true; } /* * Optionally actually grab the tool's output and include it in the same * frame. Return value is a bit complex. * Boolean.FALSE - Some kind of failure * ByteArrayServletResponse - Something that needs to be simply sent out (i.e. not bufferable) * Map - Buffering is a success and map contains buffer pieces */ public Object bufferContent(HttpServletRequest req, HttpServletResponse res, Session session, String placementId, String toolContextPath, String toolPathInfo, ToolConfiguration siteTool) { log.debug("bufferContent starting"); // Produce the buffered response ByteArrayServletResponse bufferedResponse = new ByteArrayServletResponse(res); try { // Prepare the session for the tools. Handles session reset, reseturl // and helpurl for neo tools - we don't need the returned map Map discard = portal.includeTool(res, req, siteTool, true); boolean retval = doToolBuffer(req, bufferedResponse, session, placementId, toolContextPath, toolPathInfo); log.debug("bufferContent retval=" + retval); // Cleanup transient session bits - SAK-25857 ToolSession ts = session.getToolSession(siteTool.getId()); if (ts != null) { ts.removeAttribute(Portal.SAKAI_PORTAL_ALLOW_NEO); ts.removeAttribute(Portal.SAKAI_PORTAL_HELP_ACTION); ts.removeAttribute(Portal.SAKAI_PORTAL_RESET_ACTION); } if (!retval) return Boolean.FALSE; // If the tool did a redirect - tell our caller to just complete the response if (bufferedResponse.getRedirect() != null) return bufferedResponse; // Check the response contentType for a pattern match String commonToolId = siteTool.getToolId(); String pattern = ServerConfigurationService.getString(BYPASS_TYPE_PROP, DEFAULT_BYPASS_TYPE); pattern = ServerConfigurationService.getString(BYPASS_TYPE_PROP + "." + commonToolId, pattern); if (pattern.length() > 0) { String contentType = res.getContentType(); if (contentType == null) contentType = ""; Pattern p = Pattern.compile(pattern); Matcher mc = p.matcher(contentType.toLowerCase()); if (mc.find()) return bufferedResponse; } } catch (ToolException e) { e.printStackTrace(); return Boolean.FALSE; } catch (IOException e) { e.printStackTrace(); return Boolean.FALSE; } String responseStr = bufferedResponse.getInternalBuffer(); if (responseStr == null || responseStr.length() < 1) return Boolean.FALSE; String responseStrLower = responseStr.toLowerCase(); int headStart = responseStrLower.indexOf("<head"); headStart = findEndOfTag(responseStrLower, headStart); int headEnd = responseStrLower.indexOf("</head"); int bodyStart = responseStrLower.indexOf("<body"); bodyStart = findEndOfTag(responseStrLower, bodyStart); // Some tools (Blogger for example) have multiple // head-body pairs - browsers seem to not care much about // this so we will do the same - so that we can be // somewhat clean - we search for the "last" end // body tag - for the normal case there will only be one int bodyEnd = responseStrLower.lastIndexOf("</body"); // If there is no body end at all or it is before the body // start tag we simply - take the rest of the response if (bodyEnd < bodyStart) bodyEnd = responseStrLower.length() - 1; String tidAllow = ServerConfigurationService.getString(IFRAME_SUPPRESS_PROP, IFRAME_SUPPRESS_DEFAULT); if (tidAllow.indexOf(":debug:") >= 0) log.info("Frameless HS=" + headStart + " HE=" + headEnd + " BS=" + bodyStart + " BE=" + bodyEnd); if (bodyEnd > bodyStart && bodyStart > headEnd && headEnd > headStart && headStart > 1) { Map m = new HashMap<String, String>(); String headString = responseStr.substring(headStart + 1, headEnd); String bodyString = responseStr.substring(bodyStart + 1, bodyEnd); if (tidAllow.indexOf(":debug:") >= 0) { System.out.println(" ---- Head --- "); System.out.println(headString); System.out.println(" ---- Body --- "); System.out.println(bodyString); } m.put("responseHead", headString); m.put("responseBody", bodyString); log.debug("responseHead " + headString.length() + " bytes, responseBody " + bodyString.length() + " bytes"); return m; } log.debug("bufferContent could not find head/body content"); // log.debug(responseStr); return bufferedResponse; } private int findEndOfTag(String string, int startPos) { if (startPos < 1) return -1; for (int i = startPos; i < string.length(); i++) { if (string.charAt(i) == '>') return i; } return -1; } public boolean doToolBuffer(HttpServletRequest req, HttpServletResponse res, Session session, String placementId, String toolContextPath, String toolPathInfo) throws ToolException, IOException { if (portal.redirectIfLoggedOut(res)) return false; // find the tool from some site ToolConfiguration siteTool = SiteService.findTool(placementId); if (siteTool == null) { return false; } // find the tool registered for this ActiveTool tool = ActiveToolManager.getActiveTool(siteTool.getToolId()); if (tool == null) { return false; } // permission check - visit the site (unless the tool is configured to // bypass) if (tool.getAccessSecurity() == Tool.AccessSecurity.PORTAL) { try { SiteService.getSiteVisit(siteTool.getSiteId()); } catch (IdUnusedException e) { portal.doError(req, res, session, Portal.ERROR_WORKSITE); return false; } catch (PermissionException e) { return false; } } log.debug("doToolBuffer siteTool=" + siteTool + " TCP=" + toolContextPath + " TPI=" + toolPathInfo); portal.forwardTool(tool, req, res, siteTool, siteTool.getSkin(), toolContextPath, toolPathInfo); return true; } }