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