/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/portal/trunk/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/PDAHandler.java $ * $Id: PDAHandler.java 132939 2013-12-29 16:59:50Z csev@umich.edu $ *********************************************************************************** * * 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.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.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: 132939 $ * */ @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:"; 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 ToolConfiguration siteTool = SiteService.findTool(toolId); String commonToolId = null; String toolContextPath = null; String toolPathInfo = null; boolean allowBuffer = false; Object BC = null; if ( siteTool != null && parts.length >= 5 ) { commonToolId = siteTool.getToolId(); // Does the tool allow us to buffer? allowBuffer = allowBufferContent(req, siteTool); if ( allowBuffer ) { toolContextPath = req.getContextPath() + req.getServletPath() + Web.makePath(parts, 1, 5); toolPathInfo = Web.makePath(parts, 5, parts.length); // 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; } } } // 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 (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, 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; } 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) { // Produce the buffered response ByteArrayServletResponse bufferedResponse = new ByteArrayServletResponse(res); try { boolean retval = doToolBuffer(req, bufferedResponse, session, placementId, toolContextPath, toolPathInfo); 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) { return Boolean.FALSE; } catch (IOException e) { 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); return m; } 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; } // Reset the tool state if requested if (portalService.isResetRequested(req)) { Session s = SessionManager.getCurrentSession(); ToolSession ts = s.getToolSession(placementId); ts.clearAttributes(); portalService.setResetState(null); } // 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; } }