/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.servlet; import com.liferay.portal.kernel.cache.PortalCache; import com.liferay.portal.kernel.cache.SingleVMPoolUtil; import com.liferay.portal.kernel.exception.NoSuchLayoutException; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.model.Portlet; import com.liferay.portal.kernel.model.PortletApp; import com.liferay.portal.kernel.service.PortletLocalServiceUtil; import com.liferay.portal.kernel.servlet.HttpHeaders; import com.liferay.portal.kernel.servlet.RequestDispatcherUtil; import com.liferay.portal.kernel.servlet.ServletResponseUtil; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.ContentTypes; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.HttpUtil; import com.liferay.portal.kernel.util.ObjectValuePair; import com.liferay.portal.kernel.util.ParamUtil; import com.liferay.portal.kernel.util.PortalUtil; import com.liferay.portal.kernel.util.PortletKeys; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.SetUtil; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Time; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.minifier.MinifierUtil; import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil; import com.liferay.portal.util.AggregateUtil; import com.liferay.portal.util.PrefsPropsUtil; import com.liferay.portal.util.PropsValues; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author Eduardo Lundgren * @author Edward Han * @author Zsigmond Rab * @author Raymond Augé */ public class ComboServlet extends HttpServlet { public static void clearCache() { _bytesArrayPortalCache.removeAll(); _fileContentBagPortalCache.removeAll(); } @Override public void service( HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { doService(request, response); } catch (Exception e) { _log.error(e, e); PortalUtil.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request, response); } } protected static String getModulePortletId(String modulePath) { int index = modulePath.indexOf(CharPool.COLON); if (index > 0) { return modulePath.substring(0, index); } return PortletKeys.PORTAL; } protected static String getResourcePath(String modulePath) { int index = modulePath.indexOf(CharPool.COLON); if (index > 0) { return HttpUtil.removePathParameters( modulePath.substring(index + 1)); } return HttpUtil.removePathParameters(modulePath); } protected void doService( HttpServletRequest request, HttpServletResponse response) throws Exception { Set<String> modulePathsSet = new LinkedHashSet<>(); Map<String, String[]> parameterMap = HttpUtil.getParameterMap( request.getQueryString()); Enumeration<String> enu = Collections.enumeration( parameterMap.keySet()); while (enu.hasMoreElements()) { String name = enu.nextElement(); if (_protectedParameters.contains(name)) { continue; } name = HttpUtil.decodePath(name); ServletContext servletContext = getServletContext(); String contextPath = servletContext.getContextPath(); if (name.startsWith(contextPath)) { name = name.replaceFirst(contextPath, StringPool.BLANK); } String pathProxy = PortalUtil.getPathProxy(); if (name.startsWith(pathProxy)) { name = name.replaceFirst(pathProxy, StringPool.BLANK); } modulePathsSet.add(name); } if (modulePathsSet.isEmpty()) { PortalUtil.sendError( HttpServletResponse.SC_NOT_FOUND, new NoSuchLayoutException( "Query string translates to an empty module paths set"), request, response); return; } String[] modulePaths = modulePathsSet.toArray( new String[modulePathsSet.size()]); String firstModulePath = modulePaths[0]; String resourcePath = getResourcePath(firstModulePath); String extension = FileUtil.getExtension(resourcePath); String minifierType = ParamUtil.getString(request, "minifierType"); if (Validator.isNull(minifierType)) { minifierType = "js"; if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) { minifierType = "css"; } } if (!minifierType.equals("css") && !minifierType.equals("js")) { minifierType = "js"; } String modulePathsString = null; byte[][] bytesArray = null; if (!PropsValues.COMBO_CHECK_TIMESTAMP) { modulePathsString = Arrays.toString(modulePaths); modulePathsString += StringPool.POUND + LanguageUtil.getLanguageId(request); bytesArray = _bytesArrayPortalCache.get(modulePathsString); } if (bytesArray == null) { bytesArray = new byte[modulePaths.length][]; for (int i = 0; i < modulePaths.length; i++) { String modulePath = modulePaths[i]; if (!validateModuleExtension(modulePath)) { response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE); response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } byte[] bytes = new byte[0]; if (Validator.isNotNull(modulePath)) { RequestDispatcher requestDispatcher = getResourceRequestDispatcher( request, response, modulePath); if (requestDispatcher == null) { response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE); response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } bytes = getResourceContent( requestDispatcher, request, response, modulePath, minifierType); } bytesArray[i] = bytes; } if ((modulePathsString != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) { _bytesArrayPortalCache.put(modulePathsString, bytesArray); } } String contentType = ContentTypes.TEXT_JAVASCRIPT; if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) { contentType = ContentTypes.TEXT_CSS; } response.setContentType(contentType); ServletResponseUtil.write(response, bytesArray); } protected byte[] getResourceContent( RequestDispatcher requestDispatcher, HttpServletRequest request, HttpServletResponse response, String modulePath, String minifierType) throws Exception { String resourcePath = getResourcePath(modulePath); String portletId = getModulePortletId(modulePath); Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId); if (!resourcePath.startsWith(portlet.getContextPath())) { resourcePath = portlet.getContextPath() + resourcePath; } StringBundler sb = new StringBundler(5); sb.append(resourcePath); sb.append(StringPool.QUESTION); sb.append(minifierType); sb.append("&languageId="); sb.append(ParamUtil.getString(request, "languageId")); String fileContentKey = sb.toString(); FileContentBag fileContentBag = _fileContentBagPortalCache.get( fileContentKey); if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) { return fileContentBag._fileContent; } if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) { long elapsedTime = System.currentTimeMillis() - fileContentBag._lastModified; if ((requestDispatcher != null) && (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) && (RequestDispatcherUtil.getLastModifiedTime( requestDispatcher, request, response) == fileContentBag._lastModified)) { return fileContentBag._fileContent; } _fileContentBagPortalCache.remove(fileContentKey); } if (requestDispatcher == null) { fileContentBag = _EMPTY_FILE_CONTENT_BAG; } else { ObjectValuePair<String, Long> objectValuePair = RequestDispatcherUtil.getContentAndLastModifiedTime( requestDispatcher, request, response); String stringFileContent = objectValuePair.getKey(); if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) && !StringUtil.endsWith( resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) { if (minifierType.equals("css")) { try { stringFileContent = DynamicCSSUtil.replaceToken( getServletContext(), request, stringFileContent); } catch (Exception e) { _log.error( "Unable to replace tokens in CSS " + resourcePath, e); if (_log.isDebugEnabled()) { _log.debug(stringFileContent); } response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE); } String baseURL = StringPool.BLANK; int slashIndex = resourcePath.lastIndexOf(CharPool.SLASH); if (slashIndex != -1) { baseURL = resourcePath.substring(0, slashIndex + 1); } stringFileContent = AggregateUtil.updateRelativeURLs( stringFileContent, baseURL); stringFileContent = MinifierUtil.minifyCss( stringFileContent); } else if (minifierType.equals("js")) { stringFileContent = MinifierUtil.minifyJavaScript( resourcePath, stringFileContent); stringFileContent = stringFileContent.concat( StringPool.NEW_LINE); } } fileContentBag = new FileContentBag( stringFileContent.getBytes(StringPool.UTF8), objectValuePair.getValue()); } if (PropsValues.COMBO_CHECK_TIMESTAMP) { int timeToLive = (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND); _fileContentBagPortalCache.put( fileContentKey, fileContentBag, timeToLive); } return fileContentBag._fileContent; } protected RequestDispatcher getResourceRequestDispatcher( HttpServletRequest request, HttpServletResponse response, String modulePath) throws Exception { String portletId = getModulePortletId(modulePath); Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId); if (portlet.isUndeployedPortlet()) { return null; } PortletApp portletApp = portlet.getPortletApp(); ServletContext servletContext = portletApp.getServletContext(); String resourcePath = getResourcePath(modulePath); if (!PortalUtil.isValidResourceId(resourcePath)) { if (_log.isWarnEnabled()) { _log.warn( "Invalid resource " + request.getRequestURL() + "?" + request.getQueryString()); } return null; } return servletContext.getRequestDispatcher(resourcePath); } protected boolean validateModuleExtension(String moduleName) throws Exception { moduleName = getResourcePath(moduleName); int index = moduleName.indexOf(CharPool.QUESTION); if (index != -1) { moduleName = moduleName.substring(0, index); } boolean validModuleExtension = false; String[] fileExtensions = PrefsPropsUtil.getStringArray( PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA); for (String fileExtension : fileExtensions) { if (StringPool.STAR.equals(fileExtension) || StringUtil.endsWith(moduleName, fileExtension)) { validModuleExtension = true; break; } } return validModuleExtension; } private static final String _CSS_EXTENSION = "css"; private static final String _CSS_MINIFIED_SUFFIX = "-min.css"; private static final FileContentBag _EMPTY_FILE_CONTENT_BAG = new FileContentBag(new byte[0], 0); private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js"; private static final Log _log = LogFactoryUtil.getLog(ComboServlet.class); private static final PortalCache<String, byte[][]> _bytesArrayPortalCache = SingleVMPoolUtil.getPortalCache(ComboServlet.class.getName()); private static final PortalCache<String, FileContentBag> _fileContentBagPortalCache = SingleVMPoolUtil.getPortalCache( FileContentBag.class.getName()); private final Set<String> _protectedParameters = SetUtil.fromArray( new String[] { "b", "browserId", "minifierType", "languageId", "t", "themeId", "zx" }); private static class FileContentBag implements Serializable { public FileContentBag(byte[] fileContent, long lastModifiedTime) { _fileContent = fileContent; _lastModified = lastModifiedTime; } private final byte[] _fileContent; private final long _lastModified; } }