001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.servlet;
016    
017    import com.liferay.portal.kernel.cache.PortalCache;
018    import com.liferay.portal.kernel.cache.SingleVMPoolUtil;
019    import com.liferay.portal.kernel.exception.NoSuchLayoutException;
020    import com.liferay.portal.kernel.language.LanguageUtil;
021    import com.liferay.portal.kernel.log.Log;
022    import com.liferay.portal.kernel.log.LogFactoryUtil;
023    import com.liferay.portal.kernel.model.Portlet;
024    import com.liferay.portal.kernel.model.PortletApp;
025    import com.liferay.portal.kernel.service.PortletLocalServiceUtil;
026    import com.liferay.portal.kernel.servlet.HttpHeaders;
027    import com.liferay.portal.kernel.servlet.RequestDispatcherUtil;
028    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
029    import com.liferay.portal.kernel.util.CharPool;
030    import com.liferay.portal.kernel.util.ContentTypes;
031    import com.liferay.portal.kernel.util.FileUtil;
032    import com.liferay.portal.kernel.util.HttpUtil;
033    import com.liferay.portal.kernel.util.ObjectValuePair;
034    import com.liferay.portal.kernel.util.ParamUtil;
035    import com.liferay.portal.kernel.util.PortalUtil;
036    import com.liferay.portal.kernel.util.PortletKeys;
037    import com.liferay.portal.kernel.util.PropsKeys;
038    import com.liferay.portal.kernel.util.SetUtil;
039    import com.liferay.portal.kernel.util.StringBundler;
040    import com.liferay.portal.kernel.util.StringPool;
041    import com.liferay.portal.kernel.util.StringUtil;
042    import com.liferay.portal.kernel.util.Time;
043    import com.liferay.portal.kernel.util.Validator;
044    import com.liferay.portal.minifier.MinifierUtil;
045    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
046    import com.liferay.portal.util.AggregateUtil;
047    import com.liferay.portal.util.PrefsPropsUtil;
048    import com.liferay.portal.util.PropsValues;
049    
050    import java.io.IOException;
051    import java.io.Serializable;
052    
053    import java.util.Arrays;
054    import java.util.Collections;
055    import java.util.Enumeration;
056    import java.util.LinkedHashSet;
057    import java.util.Map;
058    import java.util.Set;
059    
060    import javax.servlet.RequestDispatcher;
061    import javax.servlet.ServletContext;
062    import javax.servlet.ServletException;
063    import javax.servlet.http.HttpServlet;
064    import javax.servlet.http.HttpServletRequest;
065    import javax.servlet.http.HttpServletResponse;
066    
067    /**
068     * @author Eduardo Lundgren
069     * @author Edward Han
070     * @author Zsigmond Rab
071     * @author Raymond Aug??
072     */
073    public class ComboServlet extends HttpServlet {
074    
075            public static void clearCache() {
076                    _bytesArrayPortalCache.removeAll();
077                    _fileContentBagPortalCache.removeAll();
078            }
079    
080            @Override
081            public void service(
082                            HttpServletRequest request, HttpServletResponse response)
083                    throws IOException, ServletException {
084    
085                    try {
086                            doService(request, response);
087                    }
088                    catch (Exception e) {
089                            _log.error(e, e);
090    
091                            PortalUtil.sendError(
092                                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request,
093                                    response);
094                    }
095            }
096    
097            protected static String getModulePortletId(String modulePath) {
098                    int index = modulePath.indexOf(CharPool.COLON);
099    
100                    if (index > 0) {
101                            return modulePath.substring(0, index);
102                    }
103    
104                    return PortletKeys.PORTAL;
105            }
106    
107            protected static String getResourcePath(String modulePath) {
108                    int index = modulePath.indexOf(CharPool.COLON);
109    
110                    if (index > 0) {
111                            return HttpUtil.removePathParameters(
112                                    modulePath.substring(index + 1));
113                    }
114    
115                    return HttpUtil.removePathParameters(modulePath);
116            }
117    
118            protected void doService(
119                            HttpServletRequest request, HttpServletResponse response)
120                    throws Exception {
121    
122                    Set<String> modulePathsSet = new LinkedHashSet<>();
123    
124                    Map<String, String[]> parameterMap = HttpUtil.getParameterMap(
125                            request.getQueryString());
126    
127                    Enumeration<String> enu = Collections.enumeration(
128                            parameterMap.keySet());
129    
130                    while (enu.hasMoreElements()) {
131                            String name = enu.nextElement();
132    
133                            if (_protectedParameters.contains(name)) {
134                                    continue;
135                            }
136    
137                            name = HttpUtil.decodePath(name);
138    
139                            ServletContext servletContext = getServletContext();
140    
141                            String contextPath = servletContext.getContextPath();
142    
143                            if (name.startsWith(contextPath)) {
144                                    name = name.replaceFirst(contextPath, StringPool.BLANK);
145                            }
146    
147                            String pathProxy = PortalUtil.getPathProxy();
148    
149                            if (name.startsWith(pathProxy)) {
150                                    name = name.replaceFirst(pathProxy, StringPool.BLANK);
151                            }
152    
153                            modulePathsSet.add(name);
154                    }
155    
156                    if (modulePathsSet.isEmpty()) {
157                            PortalUtil.sendError(
158                                    HttpServletResponse.SC_NOT_FOUND,
159                                    new NoSuchLayoutException(
160                                            "Query string translates to an empty module paths set"),
161                                    request, response);
162    
163                            return;
164                    }
165    
166                    String[] modulePaths = modulePathsSet.toArray(
167                            new String[modulePathsSet.size()]);
168    
169                    String firstModulePath = modulePaths[0];
170    
171                    String resourcePath = getResourcePath(firstModulePath);
172    
173                    String extension = FileUtil.getExtension(resourcePath);
174    
175                    String minifierType = ParamUtil.getString(request, "minifierType");
176    
177                    if (Validator.isNull(minifierType)) {
178                            minifierType = "js";
179    
180                            if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
181                                    minifierType = "css";
182                            }
183                    }
184    
185                    if (!minifierType.equals("css") && !minifierType.equals("js")) {
186                            minifierType = "js";
187                    }
188    
189                    String modulePathsString = null;
190    
191                    byte[][] bytesArray = null;
192    
193                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
194                            modulePathsString = Arrays.toString(modulePaths);
195    
196                            modulePathsString +=
197                                    StringPool.POUND + LanguageUtil.getLanguageId(request);
198    
199                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
200                    }
201    
202                    if (bytesArray == null) {
203                            bytesArray = new byte[modulePaths.length][];
204    
205                            for (int i = 0; i < modulePaths.length; i++) {
206                                    String modulePath = modulePaths[i];
207    
208                                    if (!validateModuleExtension(modulePath)) {
209                                            response.setHeader(
210                                                    HttpHeaders.CACHE_CONTROL,
211                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
212                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
213    
214                                            return;
215                                    }
216    
217                                    byte[] bytes = new byte[0];
218    
219                                    if (Validator.isNotNull(modulePath)) {
220                                            RequestDispatcher requestDispatcher =
221                                                    getResourceRequestDispatcher(
222                                                            request, response, modulePath);
223    
224                                            if (requestDispatcher == null) {
225                                                    response.setHeader(
226                                                            HttpHeaders.CACHE_CONTROL,
227                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
228                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
229    
230                                                    return;
231                                            }
232    
233                                            bytes = getResourceContent(
234                                                    requestDispatcher, request, response, modulePath,
235                                                    minifierType);
236                                    }
237    
238                                    bytesArray[i] = bytes;
239                            }
240    
241                            if ((modulePathsString != null) &&
242                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
243    
244                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
245                            }
246                    }
247    
248                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
249    
250                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
251                            contentType = ContentTypes.TEXT_CSS;
252                    }
253    
254                    response.setContentType(contentType);
255    
256                    ServletResponseUtil.write(response, bytesArray);
257            }
258    
259            protected byte[] getResourceContent(
260                            RequestDispatcher requestDispatcher, HttpServletRequest request,
261                            HttpServletResponse response, String modulePath,
262                            String minifierType)
263                    throws Exception {
264    
265                    String resourcePath = getResourcePath(modulePath);
266    
267                    String portletId = getModulePortletId(modulePath);
268    
269                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
270    
271                    if (!resourcePath.startsWith(portlet.getContextPath())) {
272                            resourcePath = portlet.getContextPath() + resourcePath;
273                    }
274    
275                    StringBundler sb = new StringBundler(5);
276    
277                    sb.append(resourcePath);
278                    sb.append(StringPool.QUESTION);
279                    sb.append(minifierType);
280                    sb.append("&languageId=");
281                    sb.append(ParamUtil.getString(request, "languageId"));
282    
283                    String fileContentKey = sb.toString();
284    
285                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
286                            fileContentKey);
287    
288                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
289                            return fileContentBag._fileContent;
290                    }
291    
292                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
293                            long elapsedTime =
294                                    System.currentTimeMillis() - fileContentBag._lastModified;
295    
296                            if ((requestDispatcher != null) &&
297                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
298                                    (RequestDispatcherUtil.getLastModifiedTime(
299                                            requestDispatcher, request, response) ==
300                                                    fileContentBag._lastModified)) {
301    
302                                    return fileContentBag._fileContent;
303                            }
304    
305                            _fileContentBagPortalCache.remove(fileContentKey);
306                    }
307    
308                    if (requestDispatcher == null) {
309                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
310                    }
311                    else {
312                            ObjectValuePair<String, Long> objectValuePair =
313                                    RequestDispatcherUtil.getContentAndLastModifiedTime(
314                                            requestDispatcher, request, response);
315    
316                            String stringFileContent = objectValuePair.getKey();
317    
318                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
319                                    !StringUtil.endsWith(
320                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
321    
322                                    if (minifierType.equals("css")) {
323                                            try {
324                                                    stringFileContent = DynamicCSSUtil.replaceToken(
325                                                            getServletContext(), request, stringFileContent);
326                                            }
327                                            catch (Exception e) {
328                                                    _log.error(
329                                                            "Unable to replace tokens in CSS " + resourcePath,
330                                                            e);
331    
332                                                    if (_log.isDebugEnabled()) {
333                                                            _log.debug(stringFileContent);
334                                                    }
335    
336                                                    response.setHeader(
337                                                            HttpHeaders.CACHE_CONTROL,
338                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
339                                            }
340    
341                                            String baseURL = StringPool.BLANK;
342    
343                                            int slashIndex = resourcePath.lastIndexOf(CharPool.SLASH);
344    
345                                            if (slashIndex != -1) {
346                                                    baseURL = resourcePath.substring(0, slashIndex + 1);
347                                            }
348    
349                                            stringFileContent = AggregateUtil.updateRelativeURLs(
350                                                    stringFileContent, baseURL);
351    
352                                            stringFileContent = MinifierUtil.minifyCss(
353                                                    stringFileContent);
354                                    }
355                                    else if (minifierType.equals("js")) {
356                                            stringFileContent = MinifierUtil.minifyJavaScript(
357                                                    resourcePath, stringFileContent);
358    
359                                            stringFileContent = stringFileContent.concat(
360                                                    StringPool.NEW_LINE);
361                                    }
362                            }
363    
364                            fileContentBag = new FileContentBag(
365                                    stringFileContent.getBytes(StringPool.UTF8),
366                                    objectValuePair.getValue());
367                    }
368    
369                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
370                            int timeToLive =
371                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
372    
373                            _fileContentBagPortalCache.put(
374                                    fileContentKey, fileContentBag, timeToLive);
375                    }
376    
377                    return fileContentBag._fileContent;
378            }
379    
380            protected RequestDispatcher getResourceRequestDispatcher(
381                            HttpServletRequest request, HttpServletResponse response,
382                            String modulePath)
383                    throws Exception {
384    
385                    String portletId = getModulePortletId(modulePath);
386    
387                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
388    
389                    if (portlet.isUndeployedPortlet()) {
390                            return null;
391                    }
392    
393                    PortletApp portletApp = portlet.getPortletApp();
394    
395                    ServletContext servletContext = portletApp.getServletContext();
396    
397                    String resourcePath = getResourcePath(modulePath);
398    
399                    if (!PortalUtil.isValidResourceId(resourcePath)) {
400                            if (_log.isWarnEnabled()) {
401                                    _log.warn(
402                                            "Invalid resource " + request.getRequestURL() + "?" +
403                                                    request.getQueryString());
404                            }
405    
406                            return null;
407                    }
408    
409                    return servletContext.getRequestDispatcher(resourcePath);
410            }
411    
412            protected boolean validateModuleExtension(String moduleName)
413                    throws Exception {
414    
415                    moduleName = getResourcePath(moduleName);
416    
417                    int index = moduleName.indexOf(CharPool.QUESTION);
418    
419                    if (index != -1) {
420                            moduleName = moduleName.substring(0, index);
421                    }
422    
423                    boolean validModuleExtension = false;
424    
425                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
426                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
427    
428                    for (String fileExtension : fileExtensions) {
429                            if (StringPool.STAR.equals(fileExtension) ||
430                                    StringUtil.endsWith(moduleName, fileExtension)) {
431    
432                                    validModuleExtension = true;
433    
434                                    break;
435                            }
436                    }
437    
438                    return validModuleExtension;
439            }
440    
441            private static final String _CSS_EXTENSION = "css";
442    
443            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
444    
445            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
446                    new FileContentBag(new byte[0], 0);
447    
448            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
449    
450            private static final Log _log = LogFactoryUtil.getLog(ComboServlet.class);
451    
452            private static final PortalCache<String, byte[][]> _bytesArrayPortalCache =
453                    SingleVMPoolUtil.getPortalCache(ComboServlet.class.getName());
454            private static final PortalCache<String, FileContentBag>
455                    _fileContentBagPortalCache = SingleVMPoolUtil.getPortalCache(
456                            FileContentBag.class.getName());
457    
458            private final Set<String> _protectedParameters = SetUtil.fromArray(
459                    new String[] {
460                            "b", "browserId", "minifierType", "languageId", "t", "themeId", "zx"
461                    });
462    
463            private static class FileContentBag implements Serializable {
464    
465                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
466                            _fileContent = fileContent;
467                            _lastModified = lastModifiedTime;
468                    }
469    
470                    private final byte[] _fileContent;
471                    private final long _lastModified;
472    
473            }
474    
475    }