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                            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 = translate(
341                                                    request, portletId, stringFileContent);
342    
343                                            stringFileContent = MinifierUtil.minifyJavaScript(
344                                                    resourcePath, stringFileContent);
345                                    }
346                            }
347    
348                            fileContentBag = new FileContentBag(
349                                    stringFileContent.getBytes(StringPool.UTF8),
350                                    objectValuePair.getValue());
351                    }
352    
353                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
354                            int timeToLive =
355                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
356    
357                            _fileContentBagPortalCache.put(
358                                    fileContentKey, fileContentBag, timeToLive);
359                    }
360    
361                    return fileContentBag._fileContent;
362            }
363    
364            protected RequestDispatcher getResourceRequestDispatcher(
365                            HttpServletRequest request, HttpServletResponse response,
366                            String modulePath)
367                    throws Exception {
368    
369                    String portletId = getModulePortletId(modulePath);
370    
371                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
372    
373                    if (portlet.isUndeployedPortlet()) {
374                            return null;
375                    }
376    
377                    PortletApp portletApp = portlet.getPortletApp();
378    
379                    ServletContext servletContext = portletApp.getServletContext();
380    
381                    String resourcePath = getResourcePath(modulePath);
382    
383                    if (!PortalUtil.isValidResourceId(resourcePath)) {
384                            if (_log.isWarnEnabled()) {
385                                    _log.warn(
386                                            "Invalid resource " + request.getRequestURL() + "?" +
387                                                    request.getQueryString());
388                            }
389    
390                            return null;
391                    }
392    
393                    return servletContext.getRequestDispatcher(resourcePath);
394            }
395    
396            protected String translate(
397                    HttpServletRequest request, String portletId,
398                    String stringFileContent) {
399    
400                    String languageId = LanguageUtil.getLanguageId(request);
401    
402                    Locale locale = LocaleUtil.fromLanguageId(languageId);
403    
404                    ResourceBundle resourceBundle = LanguageResources.getResourceBundle(
405                            locale);
406    
407                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
408    
409                    if (portlet != null) {
410                            PortletConfig portletConfig = PortletConfigFactoryUtil.create(
411                                    portlet, getServletContext());
412    
413                            if (portletConfig != null) {
414                                    resourceBundle = new AggregateResourceBundle(
415                                            portletConfig.getResourceBundle(locale), resourceBundle);
416                            }
417                    }
418    
419                    return LanguageUtil.process(resourceBundle, locale, stringFileContent);
420            }
421    
422            protected boolean validateModuleExtension(String moduleName)
423                    throws Exception {
424    
425                    int index = moduleName.indexOf(CharPool.QUESTION);
426    
427                    if (index != -1) {
428                            moduleName = moduleName.substring(0, index);
429                    }
430    
431                    boolean validModuleExtension = false;
432    
433                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
434                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
435    
436                    for (String fileExtension : fileExtensions) {
437                            if (StringPool.STAR.equals(fileExtension) ||
438                                    StringUtil.endsWith(moduleName, fileExtension)) {
439    
440                                    validModuleExtension = true;
441    
442                                    break;
443                            }
444                    }
445    
446                    return validModuleExtension;
447            }
448    
449            private static final String _CSS_EXTENSION = "css";
450    
451            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
452    
453            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
454                    new FileContentBag(new byte[0], 0);
455    
456            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
457    
458            private static final Log _log = LogFactoryUtil.getLog(ComboServlet.class);
459    
460            private static final PortalCache<String, byte[][]> _bytesArrayPortalCache =
461                    SingleVMPoolUtil.getPortalCache(ComboServlet.class.getName());
462            private static final PortalCache<String, FileContentBag>
463                    _fileContentBagPortalCache = SingleVMPoolUtil.getPortalCache(
464                            FileContentBag.class.getName());
465    
466            private final Set<String> _protectedParameters = SetUtil.fromArray(
467                    new String[] {
468                            "b", "browserId", "minifierType", "languageId", "t", "themeId"
469                    });
470    
471            private static class FileContentBag implements Serializable {
472    
473                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
474                            _fileContent = fileContent;
475                            _lastModified = lastModifiedTime;
476                    }
477    
478                    private final byte[] _fileContent;
479                    private final long _lastModified;
480    
481            }
482    
483    }