com.liferay.portal.servlet.ComboServlet Maven / Gradle / Ivy
/**
* 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.petra.string.CharPool;
import com.liferay.petra.string.StringBundler;
import com.liferay.petra.string.StringPool;
import com.liferay.portal.kernel.cache.PortalCache;
import com.liferay.portal.kernel.cache.PortalCacheHelperUtil;
import com.liferay.portal.kernel.cache.PortalCacheManagerNames;
import com.liferay.portal.kernel.exception.NoSuchLayoutException;
import com.liferay.portal.kernel.frontend.source.map.FrontendSourceMapUtil;
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.BufferCacheServletResponse;
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.ContentTypes;
import com.liferay.portal.kernel.util.DigesterUtil;
import com.liferay.portal.kernel.util.FileUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HttpComponentsUtil;
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.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 java.util.regex.Matcher;
import java.util.regex.Pattern;
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 httpServletRequest,
HttpServletResponse httpServletResponse)
throws IOException, ServletException {
try {
doService(httpServletRequest, httpServletResponse);
}
catch (Exception exception) {
_log.error(exception);
PortalUtil.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR, exception,
httpServletRequest, httpServletResponse);
}
}
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 HttpComponentsUtil.removePathParameters(
modulePath.substring(index + 1));
}
return HttpComponentsUtil.removePathParameters(modulePath);
}
protected void doService(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws Exception {
Set modulePathsSet = new LinkedHashSet<>();
Map parameterMap = HttpComponentsUtil.getParameterMap(
httpServletRequest.getQueryString());
Enumeration enumeration = Collections.enumeration(
parameterMap.keySet());
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
if (_protectedParameters.contains(name)) {
continue;
}
name = HttpComponentsUtil.decodePath(name);
ServletContext servletContext = getServletContext();
String pathProxy = PortalUtil.getPathProxy();
if (name.startsWith(pathProxy)) {
name = name.replaceFirst(pathProxy, StringPool.BLANK);
}
String contextPath = servletContext.getContextPath();
if (name.startsWith(contextPath)) {
name = name.replaceFirst(contextPath, 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"),
httpServletRequest, httpServletResponse);
return;
}
String[] modulePaths = modulePathsSet.toArray(new String[0]);
String extension = StringPool.BLANK;
for (String modulePath : modulePaths) {
String pathExtension = _getModulePathExtension(modulePath);
if (Validator.isNull(pathExtension)) {
continue;
}
if (Validator.isNull(extension)) {
extension = pathExtension;
}
if (!modulePath.startsWith(_WEB_SERVER_SERVLET_FILE_ENTRY_PREFIX) &&
!extension.equals(pathExtension)) {
httpServletResponse.setHeader(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
httpServletResponse.setStatus(
HttpServletResponse.SC_BAD_REQUEST);
return;
}
}
boolean cacheEnabled = true;
if (PropsValues.WORK_DIR_OVERRIDE_ENABLED) {
cacheEnabled = false;
httpServletResponse.setHeader(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
}
String minifierType = ParamUtil.getString(
httpServletRequest, "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(httpServletRequest);
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)) {
httpServletResponse.setHeader(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
httpServletResponse.setStatus(
HttpServletResponse.SC_BAD_REQUEST);
return;
}
byte[] bytes = new byte[0];
if (Validator.isNotNull(modulePath)) {
RequestDispatcher requestDispatcher =
getResourceRequestDispatcher(
httpServletRequest, httpServletResponse,
modulePath);
if (requestDispatcher == null) {
httpServletResponse.setHeader(
HttpHeaders.CACHE_CONTROL,
HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
httpServletResponse.setStatus(
HttpServletResponse.SC_NOT_FOUND);
return;
}
bytes = getResourceContent(
requestDispatcher, httpServletRequest,
httpServletResponse, modulePath, minifierType);
}
bytesArray[i] = bytes;
}
if (cacheEnabled && (modulePathsString != null) &&
!PropsValues.COMBO_CHECK_TIMESTAMP) {
_bytesArrayPortalCache.put(modulePathsString, bytesArray);
}
}
String contentType = ContentTypes.TEXT_JAVASCRIPT;
if (StringUtil.equalsIgnoreCase(minifierType, "css")) {
contentType = ContentTypes.TEXT_CSS_UTF8;
}
httpServletResponse.setContentType(contentType);
ServletResponseUtil.write(httpServletResponse, bytesArray);
}
protected byte[] getResourceContent(
RequestDispatcher requestDispatcher,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, 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;
}
String fileContentKey = StringBundler.concat(
resourcePath, StringPool.QUESTION, minifierType, "&languageId=",
ParamUtil.getString(httpServletRequest, "languageId"));
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)) {
long lastModified = RequestDispatcherUtil.getLastModifiedTime(
requestDispatcher, httpServletRequest, httpServletResponse);
if (lastModified == fileContentBag._lastModified) {
return fileContentBag._fileContent;
}
}
_fileContentBagPortalCache.remove(fileContentKey);
}
if (requestDispatcher == null) {
fileContentBag = _EMPTY_FILE_CONTENT_BAG;
}
else {
BufferCacheServletResponse bufferCacheServletResponse =
RequestDispatcherUtil.getBufferCacheServletResponse(
requestDispatcher, httpServletRequest, httpServletResponse);
String stringFileContent = StringPool.BLANK;
String cacheControl = GetterUtil.getString(
bufferCacheServletResponse.getHeader("Cache-Control"));
String contentType = GetterUtil.getString(
bufferCacheServletResponse.getContentType());
int status = bufferCacheServletResponse.getStatus();
if (cacheControl.contains("no-cache") ||
cacheControl.contains("no-store")) {
_log.error(
"Skip " + modulePath +
" because it sent no-cache or no-store headers");
}
else if (!contentType.startsWith("application/javascript") &&
!contentType.startsWith("text/css") &&
!contentType.startsWith("text/javascript")) {
_log.error(
"Skip " + modulePath +
" because its content type is not CSS or JavaScript");
}
else if (status != HttpServletResponse.SC_OK) {
_log.error(
StringBundler.concat(
"Skip ", modulePath, " because it returns HTTP status ",
status));
}
else {
stringFileContent = bufferCacheServletResponse.getString();
}
if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_DASH_SUFFIX) &&
!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_DOT_SUFFIX) &&
!StringUtil.endsWith(
resourcePath, _JAVASCRIPT_MINIFIED_DASH_SUFFIX) &&
!StringUtil.endsWith(
resourcePath, _JAVASCRIPT_MINIFIED_DOT_SUFFIX)) {
if (minifierType.equals("css")) {
try {
stringFileContent = DynamicCSSUtil.replaceToken(
getServletContext(), httpServletRequest,
stringFileContent);
}
catch (Exception exception) {
_log.error(
"Unable to replace tokens in CSS " + resourcePath,
exception);
if (_log.isDebugEnabled()) {
_log.debug(stringFileContent);
}
httpServletResponse.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);
}
baseURL = PortalUtil.getPathProxy() + baseURL;
if (StringUtil.contains(
stringFileContent, _CSS_CHARSET_UTF_8,
StringPool.BLANK)) {
stringFileContent = StringUtil.removeSubstring(
stringFileContent, _CSS_CHARSET_UTF_8);
}
stringFileContent = AggregateUtil.updateRelativeURLs(
stringFileContent, baseURL);
stringFileContent =
FrontendSourceMapUtil.stripCSSSourceMapping(
stringFileContent);
stringFileContent = MinifierUtil.minifyCss(
stringFileContent);
}
else if (minifierType.equals("js")) {
Matcher matcher = _esModulePattern.matcher(
stringFileContent);
if (matcher.matches()) {
stringFileContent =
matcher.group(1) + "../o/" + matcher.group(3);
String identifier =
StringPool.UNDERLINE +
DigesterUtil.digestHex(modulePath);
stringFileContent = stringFileContent.replaceAll(
"esModule", identifier);
}
else {
stringFileContent = MinifierUtil.minifyJavaScript(
resourcePath, stringFileContent);
}
stringFileContent =
FrontendSourceMapUtil.stripJSSourceMapping(
stringFileContent);
stringFileContent = stringFileContent.concat(
StringPool.NEW_LINE);
}
}
else if (StringUtil.endsWith(
resourcePath, _JAVASCRIPT_MINIFIED_DASH_SUFFIX) ||
StringUtil.endsWith(
resourcePath, _JAVASCRIPT_MINIFIED_DOT_SUFFIX)) {
stringFileContent = stringFileContent.concat(
StringPool.NEW_LINE);
}
fileContentBag = new FileContentBag(
stringFileContent.getBytes(StringPool.UTF8),
GetterUtil.getLong(
bufferCacheServletResponse.getHeader(
HttpHeaders.LAST_MODIFIED),
-1));
}
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 httpServletRequest,
HttpServletResponse httpServletResponse, String modulePath)
throws Exception {
String portletId = getModulePortletId(modulePath);
Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
if ((portlet == null) || portlet.isUndeployedPortlet()) {
return null;
}
String resourcePath = getResourcePath(modulePath);
if (!StringUtil.startsWith(resourcePath, CharPool.SLASH) ||
!PortalUtil.isValidResourceId(resourcePath)) {
if (_log.isWarnEnabled()) {
_log.warn(
StringBundler.concat(
"Invalid resource ", httpServletRequest.getRequestURL(),
"?", httpServletRequest.getQueryString()));
}
return null;
}
PortletApp portletApp = portlet.getPortletApp();
ServletContext servletContext = portletApp.getServletContext();
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);
}
if (moduleName.startsWith(_WEB_SERVER_SERVLET_FILE_ENTRY_PREFIX)) {
return true;
}
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 String _getModulePathExtension(String modulePath) {
String resourcePath = getResourcePath(modulePath);
int index = resourcePath.indexOf(CharPool.QUESTION);
if (index != -1) {
resourcePath = resourcePath.substring(0, index);
}
return FileUtil.getExtension(resourcePath);
}
private static final String _CSS_CHARSET_UTF_8 = "@charset \"UTF-8\";";
private static final String _CSS_EXTENSION = "css";
private static final String _CSS_MINIFIED_DASH_SUFFIX = "-min.css";
private static final String _CSS_MINIFIED_DOT_SUFFIX = ".min.css";
private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
new FileContentBag(new byte[0], 0);
private static final String _JAVASCRIPT_MINIFIED_DASH_SUFFIX = "-min.js";
private static final String _JAVASCRIPT_MINIFIED_DOT_SUFFIX = ".min.js";
private static final String _WEB_SERVER_SERVLET_FILE_ENTRY_PREFIX =
"/documents/d/";
private static final Log _log = LogFactoryUtil.getLog(ComboServlet.class);
private static final PortalCache _bytesArrayPortalCache =
PortalCacheHelperUtil.getPortalCache(
PortalCacheManagerNames.SINGLE_VM, ComboServlet.class.getName());
private static final Pattern _esModulePattern = Pattern.compile(
"(import\\s*\\*\\s*as\\s*esModule\\s*from\\s*[\"'])((?:\\.\\./)+)(.*)",
Pattern.DOTALL);
private static final PortalCache
_fileContentBagPortalCache = PortalCacheHelperUtil.getPortalCache(
PortalCacheManagerNames.SINGLE_VM, FileContentBag.class.getName());
private final Set _protectedParameters = SetUtil.fromArray(
"browserId", "minifierType", "languageId", "t", "themeId", "zx");
private static class FileContentBag implements Serializable {
public FileContentBag(byte[] fileContent, long lastModified) {
_fileContent = fileContent;
_lastModified = lastModified;
}
private final byte[] _fileContent;
private final long _lastModified;
}
} © 2015 - 2025 Weber Informatics LLC | Privacy Policy