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.AggregateResourceBundle;
026    import com.liferay.portal.kernel.util.CharPool;
027    import com.liferay.portal.kernel.util.ContentTypes;
028    import com.liferay.portal.kernel.util.FileUtil;
029    import com.liferay.portal.kernel.util.HttpUtil;
030    import com.liferay.portal.kernel.util.LocaleUtil;
031    import com.liferay.portal.kernel.util.ObjectValuePair;
032    import com.liferay.portal.kernel.util.ParamUtil;
033    import com.liferay.portal.kernel.util.PropsKeys;
034    import com.liferay.portal.kernel.util.SetUtil;
035    import com.liferay.portal.kernel.util.StringPool;
036    import com.liferay.portal.kernel.util.StringUtil;
037    import com.liferay.portal.kernel.util.Time;
038    import com.liferay.portal.kernel.util.Validator;
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                            ServletContext servletContext = getServletContext();
145    
146                            String contextPath = servletContext.getContextPath();
147    
148                            if (name.startsWith(contextPath)) {
149                                    name = name.replaceFirst(contextPath, StringPool.BLANK);
150                            }
151    
152                            modulePathsSet.add(name);
153                    }
154    
155                    if (modulePathsSet.isEmpty()) {
156                            throw new IllegalArgumentException(
157                                    "Query string translates to an empty module paths set");
158                    }
159    
160                    String[] modulePaths = modulePathsSet.toArray(
161                            new String[modulePathsSet.size()]);
162    
163                    String firstModulePath = modulePaths[0];
164    
165                    String extension = FileUtil.getExtension(firstModulePath);
166    
167                    String minifierType = ParamUtil.getString(request, "minifierType");
168    
169                    if (Validator.isNull(minifierType)) {
170                            minifierType = "js";
171    
172                            if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
173                                    minifierType = "css";
174                            }
175                    }
176    
177                    if (!minifierType.equals("css") && !minifierType.equals("js")) {
178                            minifierType = "js";
179                    }
180    
181                    String modulePathsString = null;
182    
183                    byte[][] bytesArray = null;
184    
185                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
186                            modulePathsString = Arrays.toString(modulePaths);
187    
188                            if (minifierType.equals("css") &&
189                                    PortalUtil.isRightToLeft(request)) {
190    
191                                    modulePathsString += ".rtl";
192                            }
193                            else if (minifierType.equals("js")) {
194                                    modulePathsString +=
195                                            StringPool.POUND + LanguageUtil.getLanguageId(request);
196                            }
197    
198                            bytesArray = _bytesArrayPortalCache.get(modulePathsString);
199                    }
200    
201                    if (bytesArray == null) {
202                            bytesArray = new byte[modulePaths.length][];
203    
204                            for (int i = 0; i < modulePaths.length; i++) {
205                                    String modulePath = modulePaths[i];
206    
207                                    if (!validateModuleExtension(modulePath)) {
208                                            response.setHeader(
209                                                    HttpHeaders.CACHE_CONTROL,
210                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
211                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
212    
213                                            return;
214                                    }
215    
216                                    byte[] bytes = new byte[0];
217    
218                                    if (Validator.isNotNull(modulePath)) {
219                                            RequestDispatcher requestDispatcher =
220                                                    getResourceRequestDispatcher(
221                                                            request, response, modulePath);
222    
223                                            if (requestDispatcher == null) {
224                                                    response.setHeader(
225                                                            HttpHeaders.CACHE_CONTROL,
226                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
227                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
228    
229                                                    return;
230                                            }
231    
232                                            bytes = getResourceContent(
233                                                    requestDispatcher, request, response, modulePath,
234                                                    minifierType);
235                                    }
236    
237                                    bytesArray[i] = bytes;
238                            }
239    
240                            if ((modulePathsString != null) &&
241                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
242    
243                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
244                            }
245                    }
246    
247                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
248    
249                    if (StringUtil.equalsIgnoreCase(extension, _CSS_EXTENSION)) {
250                            contentType = ContentTypes.TEXT_CSS;
251                    }
252    
253                    response.setContentType(contentType);
254    
255                    ServletResponseUtil.write(response, bytesArray);
256            }
257    
258            protected byte[] getResourceContent(
259                            RequestDispatcher requestDispatcher, HttpServletRequest request,
260                            HttpServletResponse response, String modulePath,
261                            String minifierType)
262                    throws Exception {
263    
264                    String resourcePath = getResourcePath(modulePath);
265    
266                    String portletId = getModulePortletId(modulePath);
267    
268                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
269    
270                    if (!resourcePath.startsWith(portlet.getContextPath())) {
271                            resourcePath = portlet.getContextPath() + resourcePath;
272                    }
273    
274                    String fileContentKey = resourcePath.concat(StringPool.QUESTION).concat(
275                            minifierType);
276    
277                    FileContentBag fileContentBag = _fileContentBagPortalCache.get(
278                            fileContentKey);
279    
280                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
281                            return fileContentBag._fileContent;
282                    }
283    
284                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
285                            long elapsedTime =
286                                    System.currentTimeMillis() - fileContentBag._lastModified;
287    
288                            if ((requestDispatcher != null) &&
289                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
290                                    (RequestDispatcherUtil.getLastModifiedTime(
291                                            requestDispatcher, request, response) ==
292                                                    fileContentBag._lastModified)) {
293    
294                                    return fileContentBag._fileContent;
295                            }
296    
297                            _fileContentBagPortalCache.remove(fileContentKey);
298                    }
299    
300                    if (requestDispatcher == null) {
301                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
302                    }
303                    else {
304                            ObjectValuePair<String, Long> objectValuePair =
305                                    RequestDispatcherUtil.getContentAndLastModifiedTime(
306                                            requestDispatcher, request, response);
307    
308                            String stringFileContent = objectValuePair.getKey();
309    
310                            if (!StringUtil.endsWith(resourcePath, _CSS_MINIFIED_SUFFIX) &&
311                                    !StringUtil.endsWith(
312                                            resourcePath, _JAVASCRIPT_MINIFIED_SUFFIX)) {
313    
314                                    if (minifierType.equals("css")) {
315                                            try {
316                                                    stringFileContent = DynamicCSSUtil.replaceToken(
317                                                            getServletContext(), request, stringFileContent);
318                                            }
319                                            catch (Exception e) {
320                                                    _log.error(
321                                                            "Unable to replace tokens in CSS " + resourcePath,
322                                                            e);
323    
324                                                    if (_log.isDebugEnabled()) {
325                                                            _log.debug(stringFileContent);
326                                                    }
327    
328                                                    response.setHeader(
329                                                            HttpHeaders.CACHE_CONTROL,
330                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
331                                            }
332    
333                                            String baseURL = StringPool.BLANK;
334    
335                                            int slashIndex = resourcePath.lastIndexOf(CharPool.SLASH);
336    
337                                            if (slashIndex != -1) {
338                                                    baseURL = resourcePath.substring(0, slashIndex + 1);
339                                            }
340    
341                                            stringFileContent = AggregateUtil.updateRelativeURLs(
342                                                    stringFileContent, baseURL);
343    
344                                            stringFileContent = MinifierUtil.minifyCss(
345                                                    stringFileContent);
346                                    }
347                                    else if (minifierType.equals("js")) {
348                                            stringFileContent = translate(
349                                                    request, portletId, stringFileContent);
350    
351                                            stringFileContent = MinifierUtil.minifyJavaScript(
352                                                    resourcePath, stringFileContent);
353                                    }
354                            }
355    
356                            fileContentBag = new FileContentBag(
357                                    stringFileContent.getBytes(StringPool.UTF8),
358                                    objectValuePair.getValue());
359                    }
360    
361                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
362                            int timeToLive =
363                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
364    
365                            _fileContentBagPortalCache.put(
366                                    fileContentKey, fileContentBag, timeToLive);
367                    }
368    
369                    return fileContentBag._fileContent;
370            }
371    
372            protected RequestDispatcher getResourceRequestDispatcher(
373                            HttpServletRequest request, HttpServletResponse response,
374                            String modulePath)
375                    throws Exception {
376    
377                    String portletId = getModulePortletId(modulePath);
378    
379                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
380    
381                    if (portlet.isUndeployedPortlet()) {
382                            return null;
383                    }
384    
385                    PortletApp portletApp = portlet.getPortletApp();
386    
387                    ServletContext servletContext = portletApp.getServletContext();
388    
389                    String resourcePath = getResourcePath(modulePath);
390    
391                    if (!PortalUtil.isValidResourceId(resourcePath)) {
392                            if (_log.isWarnEnabled()) {
393                                    _log.warn(
394                                            "Invalid resource " + request.getRequestURL() + "?" +
395                                                    request.getQueryString());
396                            }
397    
398                            return null;
399                    }
400    
401                    return servletContext.getRequestDispatcher(resourcePath);
402            }
403    
404            protected String translate(
405                    HttpServletRequest request, String portletId,
406                    String stringFileContent) {
407    
408                    String languageId = LanguageUtil.getLanguageId(request);
409    
410                    Locale locale = LocaleUtil.fromLanguageId(languageId);
411    
412                    ResourceBundle resourceBundle = LanguageResources.getResourceBundle(
413                            locale);
414    
415                    Portlet portlet = PortletLocalServiceUtil.getPortletById(portletId);
416    
417                    if (portlet != null) {
418                            PortletConfig portletConfig = PortletConfigFactoryUtil.create(
419                                    portlet, getServletContext());
420    
421                            if (portletConfig != null) {
422                                    resourceBundle = new AggregateResourceBundle(
423                                            portletConfig.getResourceBundle(locale), resourceBundle);
424                            }
425                    }
426    
427                    return LanguageUtil.process(resourceBundle, locale, stringFileContent);
428            }
429    
430            protected boolean validateModuleExtension(String moduleName)
431                    throws Exception {
432    
433                    int index = moduleName.indexOf(CharPool.QUESTION);
434    
435                    if (index != -1) {
436                            moduleName = moduleName.substring(0, index);
437                    }
438    
439                    boolean validModuleExtension = false;
440    
441                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
442                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
443    
444                    for (String fileExtension : fileExtensions) {
445                            if (StringPool.STAR.equals(fileExtension) ||
446                                    StringUtil.endsWith(moduleName, fileExtension)) {
447    
448                                    validModuleExtension = true;
449    
450                                    break;
451                            }
452                    }
453    
454                    return validModuleExtension;
455            }
456    
457            private static final String _CSS_EXTENSION = "css";
458    
459            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
460    
461            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
462                    new FileContentBag(new byte[0], 0);
463    
464            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
465    
466            private static final Log _log = LogFactoryUtil.getLog(ComboServlet.class);
467    
468            private static final PortalCache<String, byte[][]> _bytesArrayPortalCache =
469                    SingleVMPoolUtil.getPortalCache(ComboServlet.class.getName());
470            private static final PortalCache<String, FileContentBag>
471                    _fileContentBagPortalCache = SingleVMPoolUtil.getPortalCache(
472                            FileContentBag.class.getName());
473    
474            private final Set<String> _protectedParameters = SetUtil.fromArray(
475                    new String[] {
476                            "b", "browserId", "minifierType", "languageId", "t", "themeId"
477                    });
478    
479            private static class FileContentBag implements Serializable {
480    
481                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
482                            _fileContent = fileContent;
483                            _lastModified = lastModifiedTime;
484                    }
485    
486                    private final byte[] _fileContent;
487                    private final long _lastModified;
488    
489            }
490    
491    }