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