001    /**
002     * Copyright (c) 2000-2013 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.filters.minifier;
016    
017    import com.liferay.portal.kernel.cache.key.CacheKeyGenerator;
018    import com.liferay.portal.kernel.cache.key.CacheKeyGeneratorUtil;
019    import com.liferay.portal.kernel.configuration.Filter;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.servlet.BrowserSniffer;
023    import com.liferay.portal.kernel.servlet.HttpHeaders;
024    import com.liferay.portal.kernel.servlet.ServletContextUtil;
025    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
026    import com.liferay.portal.kernel.servlet.StringServletResponse;
027    import com.liferay.portal.kernel.util.ArrayUtil;
028    import com.liferay.portal.kernel.util.CharPool;
029    import com.liferay.portal.kernel.util.ContentTypes;
030    import com.liferay.portal.kernel.util.FileUtil;
031    import com.liferay.portal.kernel.util.GetterUtil;
032    import com.liferay.portal.kernel.util.ParamUtil;
033    import com.liferay.portal.kernel.util.PropsKeys;
034    import com.liferay.portal.kernel.util.StringBundler;
035    import com.liferay.portal.kernel.util.StringPool;
036    import com.liferay.portal.kernel.util.StringUtil;
037    import com.liferay.portal.kernel.util.SystemProperties;
038    import com.liferay.portal.kernel.util.Validator;
039    import com.liferay.portal.servlet.filters.BasePortalFilter;
040    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
041    import com.liferay.portal.util.JavaScriptBundleUtil;
042    import com.liferay.portal.util.LimitedFilesCache;
043    import com.liferay.portal.util.MinifierUtil;
044    import com.liferay.portal.util.PropsUtil;
045    import com.liferay.portal.util.PropsValues;
046    import com.liferay.util.servlet.filters.CacheResponseUtil;
047    
048    import java.io.File;
049    import java.io.IOException;
050    
051    import java.net.URL;
052    import java.net.URLConnection;
053    
054    import java.util.regex.Matcher;
055    import java.util.regex.Pattern;
056    
057    import javax.servlet.FilterChain;
058    import javax.servlet.FilterConfig;
059    import javax.servlet.ServletContext;
060    import javax.servlet.http.HttpServletRequest;
061    import javax.servlet.http.HttpServletResponse;
062    
063    /**
064     * @author Brian Wing Shun Chan
065     */
066    public class MinifierFilter extends BasePortalFilter {
067    
068            /**
069             * @see {@link DynamicCSSUtil#_propagateQueryString(String, String)}
070             */
071            public static String aggregateCss(String dir, String content)
072                    throws IOException {
073    
074                    StringBundler sb = new StringBundler();
075    
076                    int pos = 0;
077    
078                    while (true) {
079                            int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos);
080                            int commentY = content.indexOf(
081                                    _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length());
082    
083                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
084                            int importY = content.indexOf(
085                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
086    
087                            if ((importX == -1) || (importY == -1)) {
088                                    sb.append(content.substring(pos));
089    
090                                    break;
091                            }
092                            else if ((commentX != -1) && (commentY != -1) &&
093                                             (commentX < importX) && (commentY > importX)) {
094    
095                                    commentY += _CSS_COMMENT_END.length();
096    
097                                    sb.append(content.substring(pos, commentY));
098    
099                                    pos = commentY;
100                            }
101                            else {
102                                    sb.append(content.substring(pos, importX));
103    
104                                    String mediaQuery = StringPool.BLANK;
105    
106                                    int mediaQueryImportX = content.indexOf(
107                                            CharPool.CLOSE_PARENTHESIS,
108                                            importX + _CSS_IMPORT_BEGIN.length());
109                                    int mediaQueryImportY = content.indexOf(
110                                            CharPool.SEMICOLON, importX + _CSS_IMPORT_BEGIN.length());
111    
112                                    String importFileName = null;
113    
114                                    if (importY != mediaQueryImportX) {
115                                            mediaQuery = content.substring(
116                                                    mediaQueryImportX + 1, mediaQueryImportY);
117    
118                                            importFileName = content.substring(
119                                                    importX + _CSS_IMPORT_BEGIN.length(),
120                                                    mediaQueryImportX);
121                                    }
122                                    else {
123                                            importFileName = content.substring(
124                                                    importX + _CSS_IMPORT_BEGIN.length(), importY);
125                                    }
126    
127                                    String importFullFileName = dir.concat(StringPool.SLASH).concat(
128                                            importFileName);
129    
130                                    String importContent = null;
131    
132                                    if (Validator.isUrl(importFileName)) {
133                                            URL resourceURL = new URL(importFileName);
134    
135                                            URLConnection urlConnection = resourceURL.openConnection();
136    
137                                            importContent = StringUtil.read(
138                                                    urlConnection.getInputStream());
139                                    }
140                                    else {
141                                            importContent = FileUtil.read(importFullFileName);
142                                    }
143    
144                                    if (importContent == null) {
145                                            if (_log.isWarnEnabled()) {
146                                                    _log.warn(
147                                                            "File " + importFullFileName + " does not exist");
148                                            }
149    
150                                            importContent = StringPool.BLANK;
151                                    }
152    
153                                    String importDir = StringPool.BLANK;
154    
155                                    int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
156    
157                                    if (slashPos != -1) {
158                                            importDir = StringPool.SLASH.concat(
159                                                    importFileName.substring(0, slashPos + 1));
160                                    }
161    
162                                    importContent = aggregateCss(dir + importDir, importContent);
163    
164                                    int importDepth = StringUtil.count(
165                                            importFileName, StringPool.SLASH);
166    
167                                    // LEP-7540
168    
169                                    String relativePath = StringPool.BLANK;
170    
171                                    for (int i = 0; i < importDepth; i++) {
172                                            relativePath += "../";
173                                    }
174    
175                                    importContent = StringUtil.replace(
176                                            importContent,
177                                            new String[] {
178                                                    "url('" + relativePath, "url(\"" + relativePath,
179                                                    "url(" + relativePath
180                                            },
181                                            new String[] {
182                                                    "url('[$TEMP_RELATIVE_PATH$]",
183                                                    "url(\"[$TEMP_RELATIVE_PATH$]",
184                                                    "url([$TEMP_RELATIVE_PATH$]"
185                                            });
186    
187                                    importContent = StringUtil.replace(
188                                            importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
189    
190                                    if (Validator.isNotNull(mediaQuery)) {
191                                            sb.append(_CSS_MEDIA_QUERY);
192                                            sb.append(CharPool.SPACE);
193                                            sb.append(mediaQuery);
194                                            sb.append(CharPool.OPEN_CURLY_BRACE);
195                                            sb.append(importContent);
196                                            sb.append(CharPool.CLOSE_CURLY_BRACE);
197    
198                                            pos = mediaQueryImportY + 1;
199                                    }
200                                    else {
201                                            sb.append(importContent);
202    
203                                            pos = importY + _CSS_IMPORT_END.length();
204                                    }
205                            }
206                    }
207    
208                    return sb.toString();
209            }
210    
211            @Override
212            public void init(FilterConfig filterConfig) {
213                    super.init(filterConfig);
214    
215                    _servletContext = filterConfig.getServletContext();
216                    _servletContextName = GetterUtil.getString(
217                            _servletContext.getServletContextName());
218    
219                    if (Validator.isNull(_servletContextName)) {
220                            _tempDir += "/portal";
221                    }
222    
223                    if (PropsValues.MINIFIER_FILES_LIMIT > 0) {
224                            _limitedFilesCache = new LimitedFilesCache<String>(
225                                    PropsValues.MINIFIER_FILES_LIMIT);
226    
227                            if (_log.isDebugEnabled()) {
228                                    _log.debug(
229                                            "Minifier files limit " + PropsValues.MINIFIER_FILES_LIMIT);
230                            }
231                    }
232            }
233    
234            protected String getCacheFileName(HttpServletRequest request) {
235                    CacheKeyGenerator cacheKeyGenerator =
236                            CacheKeyGeneratorUtil.getCacheKeyGenerator(
237                                    MinifierFilter.class.getName());
238    
239                    cacheKeyGenerator.append(request.getRequestURI());
240    
241                    String queryString = request.getQueryString();
242    
243                    if (queryString != null) {
244                            cacheKeyGenerator.append(sterilizeQueryString(queryString));
245                    }
246    
247                    String cacheKey = String.valueOf(cacheKeyGenerator.finish());
248    
249                    return _tempDir.concat(StringPool.SLASH).concat(cacheKey);
250            }
251    
252            protected Object getMinifiedBundleContent(
253                            HttpServletRequest request, HttpServletResponse response)
254                    throws IOException {
255    
256                    String minifierType = ParamUtil.getString(request, "minifierType");
257                    String minifierBundleId = ParamUtil.getString(
258                            request, "minifierBundleId");
259    
260                    if (Validator.isNull(minifierType) ||
261                            Validator.isNull(minifierBundleId) ||
262                            !ArrayUtil.contains(
263                                    PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
264    
265                            return null;
266                    }
267    
268                    String minifierBundleDir = PropsUtil.get(
269                            PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
270    
271                    String bundleDirRealPath = ServletContextUtil.getRealPath(
272                            _servletContext, minifierBundleDir);
273    
274                    if (bundleDirRealPath == null) {
275                            return null;
276                    }
277    
278                    String cacheFileName = getCacheFileName(request);
279    
280                    String[] fileNames = JavaScriptBundleUtil.getFileNames(
281                            minifierBundleId);
282    
283                    File cacheFile = new File(cacheFileName);
284    
285                    if (_limitedFilesCache != null) {
286                            _limitedFilesCache.put(cacheFileName);
287                    }
288    
289                    if (cacheFile.exists()) {
290                            boolean staleCache = false;
291    
292                            for (String fileName : fileNames) {
293                                    File file = new File(
294                                            bundleDirRealPath + StringPool.SLASH + fileName);
295    
296                                    if (file.lastModified() > cacheFile.lastModified()) {
297                                            staleCache = true;
298    
299                                            break;
300                                    }
301                            }
302    
303                            if (!staleCache) {
304                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
305    
306                                    return cacheFile;
307                            }
308                    }
309    
310                    if (_log.isInfoEnabled()) {
311                            _log.info("Minifying JavaScript bundle " + minifierBundleId);
312                    }
313    
314                    String minifiedContent = null;
315    
316                    if (fileNames.length == 0) {
317                            minifiedContent = StringPool.BLANK;
318                    }
319                    else {
320                            StringBundler sb = new StringBundler(fileNames.length * 2);
321    
322                            for (String fileName : fileNames) {
323                                    String content = FileUtil.read(
324                                            bundleDirRealPath + StringPool.SLASH + fileName);
325    
326                                    sb.append(content);
327                                    sb.append(StringPool.NEW_LINE);
328                            }
329    
330                            minifiedContent = minifyJavaScript(sb.toString());
331                    }
332    
333                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
334    
335                    FileUtil.write(cacheFile, minifiedContent);
336    
337                    return minifiedContent;
338            }
339    
340            protected Object getMinifiedContent(
341                            HttpServletRequest request, HttpServletResponse response,
342                            FilterChain filterChain)
343                    throws Exception {
344    
345                    String minifierType = ParamUtil.getString(request, "minifierType");
346                    String minifierBundleId = ParamUtil.getString(
347                            request, "minifierBundleId");
348                    String minifierBundleDir = ParamUtil.getString(
349                            request, "minifierBundleDir");
350    
351                    if (Validator.isNull(minifierType) ||
352                            Validator.isNotNull(minifierBundleId) ||
353                            Validator.isNotNull(minifierBundleDir)) {
354    
355                            return null;
356                    }
357    
358                    String requestURI = request.getRequestURI();
359    
360                    String requestPath = requestURI;
361    
362                    String contextPath = request.getContextPath();
363    
364                    if (!contextPath.equals(StringPool.SLASH)) {
365                            requestPath = requestPath.substring(contextPath.length());
366                    }
367    
368                    String realPath = ServletContextUtil.getRealPath(
369                            _servletContext, requestPath);
370    
371                    if (realPath == null) {
372                            return null;
373                    }
374    
375                    realPath = StringUtil.replace(
376                            realPath, CharPool.BACK_SLASH, CharPool.SLASH);
377    
378                    File file = new File(realPath);
379    
380                    if (!file.exists()) {
381                            return null;
382                    }
383    
384                    String cacheCommonFileName = getCacheFileName(request);
385    
386                    File cacheContentTypeFile = new File(
387                            cacheCommonFileName + "_E_CONTENT_TYPE");
388                    File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
389    
390                    if (cacheDataFile.exists() &&
391                            (cacheDataFile.lastModified() >= file.lastModified())) {
392    
393                            if (cacheContentTypeFile.exists()) {
394                                    String contentType = FileUtil.read(cacheContentTypeFile);
395    
396                                    response.setContentType(contentType);
397                            }
398    
399                            return cacheDataFile;
400                    }
401    
402                    String minifiedContent = null;
403    
404                    if (realPath.endsWith(_CSS_EXTENSION)) {
405                            if (_log.isInfoEnabled()) {
406                                    _log.info("Minifying CSS " + file);
407                            }
408    
409                            minifiedContent = minifyCss(request, response, file);
410    
411                            response.setContentType(ContentTypes.TEXT_CSS);
412    
413                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
414                    }
415                    else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
416                            if (_log.isInfoEnabled()) {
417                                    _log.info("Minifying JavaScript " + file);
418                            }
419    
420                            minifiedContent = minifyJavaScript(file);
421    
422                            response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
423    
424                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
425                    }
426                    else if (realPath.endsWith(_JSP_EXTENSION)) {
427                            if (_log.isInfoEnabled()) {
428                                    _log.info("Minifying JSP " + file);
429                            }
430    
431                            StringServletResponse stringResponse = new StringServletResponse(
432                                    response);
433    
434                            processFilter(
435                                    MinifierFilter.class, request, stringResponse, filterChain);
436    
437                            CacheResponseUtil.setHeaders(response, stringResponse.getHeaders());
438    
439                            response.setContentType(stringResponse.getContentType());
440    
441                            minifiedContent = stringResponse.getString();
442    
443                            if (minifierType.equals("css")) {
444                                    minifiedContent = minifyCss(
445                                            request, response, realPath, minifiedContent);
446                            }
447                            else if (minifierType.equals("js")) {
448                                    minifiedContent = minifyJavaScript(minifiedContent);
449                            }
450    
451                            FileUtil.write(
452                                    cacheContentTypeFile, stringResponse.getContentType());
453                    }
454                    else {
455                            return null;
456                    }
457    
458                    FileUtil.write(cacheDataFile, minifiedContent);
459    
460                    return minifiedContent;
461            }
462    
463            protected String minifyCss(
464                            HttpServletRequest request, HttpServletResponse response, File file)
465                    throws IOException {
466    
467                    String content = FileUtil.read(file);
468    
469                    content = aggregateCss(file.getParent(), content);
470    
471                    return minifyCss(request, response, file.getAbsolutePath(), content);
472            }
473    
474            protected String minifyCss(
475                    HttpServletRequest request, HttpServletResponse response,
476                    String cssRealPath, String content) {
477    
478                    try {
479                            content = DynamicCSSUtil.parseSass(request, cssRealPath, content);
480                    }
481                    catch (Exception e) {
482                            _log.error("Unable to parse SASS on CSS " + cssRealPath, e);
483    
484                            if (_log.isDebugEnabled()) {
485                                    _log.debug(content);
486                            }
487    
488                            response.setHeader(
489                                    HttpHeaders.CACHE_CONTROL,
490                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
491                    }
492    
493                    String browserId = ParamUtil.getString(request, "browserId");
494    
495                    if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
496                            Matcher matcher = _pattern.matcher(content);
497    
498                            content = matcher.replaceAll(StringPool.BLANK);
499                    }
500    
501                    return MinifierUtil.minifyCss(content);
502            }
503    
504            protected String minifyJavaScript(File file) throws IOException {
505                    String content = FileUtil.read(file);
506    
507                    return minifyJavaScript(content);
508            }
509    
510            protected String minifyJavaScript(String content) {
511                    return MinifierUtil.minifyJavaScript(content);
512            }
513    
514            @Override
515            protected void processFilter(
516                            HttpServletRequest request, HttpServletResponse response,
517                            FilterChain filterChain)
518                    throws Exception {
519    
520                    Object minifiedContent = getMinifiedContent(
521                            request, response, filterChain);
522    
523                    if (minifiedContent == null) {
524                            minifiedContent = getMinifiedBundleContent(request, response);
525                    }
526    
527                    if (minifiedContent == null) {
528                            processFilter(MinifierFilter.class, request, response, filterChain);
529                    }
530                    else {
531                            if (minifiedContent instanceof File) {
532                                    ServletResponseUtil.write(response, (File)minifiedContent);
533                            }
534                            else if (minifiedContent instanceof String) {
535                                    ServletResponseUtil.write(response, (String)minifiedContent);
536                            }
537                    }
538            }
539    
540            protected String sterilizeQueryString(String queryString) {
541                    return StringUtil.replace(
542                            queryString, new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
543                            new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
544            }
545    
546            private static final String _CSS_COMMENT_BEGIN = "/*";
547    
548            private static final String _CSS_COMMENT_END = "*/";
549    
550            private static final String _CSS_EXTENSION = ".css";
551    
552            private static final String _CSS_IMPORT_BEGIN = "@import url(";
553    
554            private static final String _CSS_IMPORT_END = ");";
555    
556            private static final String _CSS_MEDIA_QUERY = "@media";
557    
558            private static final String _JAVASCRIPT_EXTENSION = ".js";
559    
560            private static final String _JSP_EXTENSION = ".jsp";
561    
562            private static final String _TEMP_DIR =
563                    SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
564    
565            private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
566    
567            private static Pattern _pattern = Pattern.compile(
568                    "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
569    
570            private LimitedFilesCache<String> _limitedFilesCache;
571            private ServletContext _servletContext;
572            private String _servletContextName;
573            private String _tempDir = _TEMP_DIR;
574    
575    }