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