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.filters.dynamiccss;
016    
017    import com.liferay.portal.kernel.log.Log;
018    import com.liferay.portal.kernel.log.LogFactoryUtil;
019    import com.liferay.portal.kernel.util.CharPool;
020    import com.liferay.portal.kernel.util.FileUtil;
021    import com.liferay.portal.kernel.util.JavaConstants;
022    import com.liferay.portal.kernel.util.ParamUtil;
023    import com.liferay.portal.kernel.util.ServerDetector;
024    import com.liferay.portal.kernel.util.SessionParamUtil;
025    import com.liferay.portal.kernel.util.StringBundler;
026    import com.liferay.portal.kernel.util.StringPool;
027    import com.liferay.portal.kernel.util.StringUtil;
028    import com.liferay.portal.kernel.util.URLUtil;
029    import com.liferay.portal.kernel.util.Validator;
030    import com.liferay.portal.kernel.util.WebKeys;
031    import com.liferay.portal.model.PortletConstants;
032    import com.liferay.portal.model.Theme;
033    import com.liferay.portal.service.ThemeLocalServiceUtil;
034    import com.liferay.portal.theme.ThemeDisplay;
035    import com.liferay.portal.tools.CSSBuilderUtil;
036    import com.liferay.portal.util.PortalUtil;
037    import com.liferay.portal.util.PropsValues;
038    import com.liferay.sass.compiler.SassCompiler;
039    import com.liferay.sass.compiler.jni.internal.JniSassCompiler;
040    import com.liferay.sass.compiler.ruby.internal.RubySassCompiler;
041    
042    import java.io.File;
043    
044    import java.net.URL;
045    import java.net.URLConnection;
046    import java.net.URLDecoder;
047    
048    import java.util.regex.Matcher;
049    import java.util.regex.Pattern;
050    
051    import javax.servlet.ServletContext;
052    import javax.servlet.http.HttpServletRequest;
053    
054    import org.apache.commons.lang.time.StopWatch;
055    
056    /**
057     * @author Raymond Aug??
058     * @author Sergio S??nchez
059     * @author David Truong
060     */
061    public class DynamicCSSUtil {
062    
063            public static void init(ServletContext servletContext) {
064                    try {
065                            if (_initialized) {
066                                    return;
067                            }
068    
069                            try {
070                                    _sassCompiler = new JniSassCompiler();
071                            }
072                            catch (Throwable t) {
073                                    File sassTempDir = _getSassTempDir(servletContext);
074    
075                                    _sassCompiler = new RubySassCompiler(
076                                            PropsValues.SCRIPTING_JRUBY_COMPILE_MODE,
077                                            PropsValues.SCRIPTING_JRUBY_COMPILE_THRESHOLD,
078                                            sassTempDir.getCanonicalPath());
079                            }
080    
081                            _initialized = true;
082                    }
083                    catch (Exception e) {
084                            _log.error(e, e);
085                    }
086            }
087    
088            public static String parseSass(
089                            ServletContext servletContext, HttpServletRequest request,
090                            String resourcePath, String content)
091                    throws Exception {
092    
093                    if (!DynamicCSSFilter.ENABLED) {
094                            return content;
095                    }
096    
097                    StopWatch stopWatch = new StopWatch();
098    
099                    stopWatch.start();
100    
101                    // Request will only be null when called by StripFilterTest
102    
103                    if (request == null) {
104                            return content;
105                    }
106    
107                    ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
108                            WebKeys.THEME_DISPLAY);
109    
110                    Theme theme = null;
111    
112                    if (themeDisplay == null) {
113                            theme = _getTheme(request);
114    
115                            if (theme == null) {
116                                    String currentURL = PortalUtil.getCurrentURL(request);
117    
118                                    if (_log.isWarnEnabled()) {
119                                            _log.warn("No theme found for " + currentURL);
120                                    }
121    
122                                    if (PortalUtil.isRightToLeft(request) &&
123                                            !RTLCSSUtil.isExcludedPath(resourcePath)) {
124    
125                                            content = RTLCSSUtil.getRtlCss(resourcePath, content);
126                                    }
127    
128                                    return content;
129                            }
130                    }
131    
132                    String parsedContent = null;
133    
134                    boolean themeCssFastLoad = _isThemeCssFastLoad(request, themeDisplay);
135    
136                    URL cacheResourceURL = _getCacheResourceURL(
137                            servletContext, request, resourcePath);
138    
139                    if (cacheResourceURL != null) {
140                            if (!themeCssFastLoad) {
141                                    URL resourceURL = servletContext.getResource(resourcePath);
142    
143                                    if (resourceURL != null) {
144                                            if (URLUtil.getLastModifiedTime(cacheResourceURL) <
145                                                            URLUtil.getLastModifiedTime(resourceURL)) {
146    
147                                                    cacheResourceURL = null;
148                                            }
149                                    }
150                            }
151                    }
152    
153                    if ((themeCssFastLoad || !content.contains(_CSS_IMPORT_BEGIN)) &&
154                            (cacheResourceURL != null)) {
155    
156                            parsedContent = StringUtil.read(cacheResourceURL.openStream());
157    
158                            if (_log.isDebugEnabled()) {
159                                    _log.debug(
160                                            "Loading SASS cache from " + cacheResourceURL.getPath() +
161                                                    " takes " + stopWatch.getTime() + " ms");
162                            }
163                    }
164                    else {
165                            content = CSSBuilderUtil.parseStaticTokens(content);
166    
167                            String queryString = request.getQueryString();
168    
169                            if (!themeCssFastLoad && Validator.isNotNull(queryString)) {
170                                    content = propagateQueryString(content, queryString);
171                            }
172    
173                            if (!themeCssFastLoad && _isImportsOnly(content)) {
174                                    parsedContent = content;
175                            }
176                            else {
177                                    parsedContent = _parseSass(
178                                            servletContext, request, themeDisplay, theme, content);
179                            }
180    
181                            if (PortalUtil.isRightToLeft(request) &&
182                                    !RTLCSSUtil.isExcludedPath(resourcePath)) {
183    
184                                    parsedContent = RTLCSSUtil.getRtlCss(
185                                            resourcePath, parsedContent);
186    
187                                    // Append custom CSS for RTL
188    
189                                    URL rtlCustomResourceURL = _getRtlCustomResourceURL(
190                                            servletContext, resourcePath);
191    
192                                    if (rtlCustomResourceURL != null) {
193                                            URLConnection rtlCustomResourceURLConnection =
194                                                    rtlCustomResourceURL.openConnection();
195    
196                                            String rtlCustomContent = StringUtil.read(
197                                                    rtlCustomResourceURLConnection.getInputStream());
198    
199                                            String parsedRtlCustomContent = _parseSass(
200                                                    servletContext, request, themeDisplay, theme,
201                                                    rtlCustomContent);
202    
203                                            parsedContent += parsedRtlCustomContent;
204                                    }
205                            }
206    
207                            if (_log.isDebugEnabled()) {
208                                    _log.debug(
209                                            "Parsing SASS for " + resourcePath + " takes " +
210                                                    stopWatch.getTime() + " ms");
211                            }
212                    }
213    
214                    if (Validator.isNull(parsedContent)) {
215                            return content;
216                    }
217    
218                    parsedContent = replaceToken(
219                            servletContext, request, themeDisplay, theme, parsedContent);
220    
221                    return parsedContent;
222            }
223    
224            public static String replaceToken(
225                            ServletContext servletContext, HttpServletRequest request,
226                            String content)
227                    throws Exception {
228    
229                    ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
230                            WebKeys.THEME_DISPLAY);
231    
232                    Theme theme = _getTheme(request);
233    
234                    if (theme == null) {
235                            return content;
236                    }
237    
238                    return replaceToken(
239                            servletContext, request, themeDisplay, theme, content);
240            }
241    
242            public static String replaceToken(
243                            ServletContext servletContext, HttpServletRequest request,
244                            ThemeDisplay themeDisplay, Theme theme, String parsedContent)
245                    throws Exception {
246    
247                    String portalContextPath = PortalUtil.getPathContext();
248    
249                    String baseURL = servletContext.getContextPath();
250    
251                    if (baseURL.endsWith(StringPool.SLASH)) {
252                            baseURL = baseURL.substring(0, baseURL.length() - 1);
253                    }
254    
255                    parsedContent = StringUtil.replace(
256                            parsedContent,
257                            new String[] {"@base_url@", "@portal_ctx@", "@theme_image_path@"},
258                            new String[] {
259                                    baseURL, portalContextPath,
260                                    _getThemeImagesPath(request, themeDisplay, theme)
261                            });
262    
263                    return parsedContent;
264            }
265    
266            /**
267             * @see com.liferay.portal.servlet.filters.aggregate.AggregateFilter#aggregateCss(
268             *      com.liferay.portal.servlet.filters.aggregate.ServletPaths, String)
269             */
270            protected static String propagateQueryString(
271                    String content, String queryString) {
272    
273                    StringBuilder sb = new StringBuilder(content.length());
274    
275                    int pos = 0;
276    
277                    while (true) {
278                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
279                            int importY = content.indexOf(
280                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
281    
282                            if ((importX == -1) || (importY == -1)) {
283                                    sb.append(content.substring(pos));
284    
285                                    break;
286                            }
287    
288                            sb.append(content.substring(pos, importX));
289                            sb.append(_CSS_IMPORT_BEGIN);
290    
291                            String url = content.substring(
292                                    importX + _CSS_IMPORT_BEGIN.length(), importY);
293    
294                            char firstChar = url.charAt(0);
295    
296                            if (firstChar == CharPool.APOSTROPHE) {
297                                    sb.append(CharPool.APOSTROPHE);
298                            }
299                            else if (firstChar == CharPool.QUOTE) {
300                                    sb.append(CharPool.QUOTE);
301                            }
302    
303                            url = StringUtil.unquote(url);
304    
305                            sb.append(url);
306    
307                            if (url.indexOf(CharPool.QUESTION) != -1) {
308                                    sb.append(CharPool.AMPERSAND);
309                            }
310                            else {
311                                    sb.append(CharPool.QUESTION);
312                            }
313    
314                            sb.append(queryString);
315    
316                            if (firstChar == CharPool.APOSTROPHE) {
317                                    sb.append(CharPool.APOSTROPHE);
318                            }
319                            else if (firstChar == CharPool.QUOTE) {
320                                    sb.append(CharPool.QUOTE);
321                            }
322    
323                            sb.append(_CSS_IMPORT_END);
324    
325                            pos = importY + _CSS_IMPORT_END.length();
326                    }
327    
328                    return sb.toString();
329            }
330    
331            private static URL _getCacheResourceURL(
332                            ServletContext servletContext, HttpServletRequest request,
333                            String resourcePath)
334                    throws Exception {
335    
336                    String suffix = StringPool.BLANK;
337    
338                    if (PortalUtil.isRightToLeft(request) &&
339                            !RTLCSSUtil.isExcludedPath(resourcePath)) {
340    
341                            suffix = "_rtl";
342                    }
343    
344                    return servletContext.getResource(
345                            CSSBuilderUtil.getCacheFileName(resourcePath, suffix));
346            }
347    
348            private static String _getCssThemePath(
349                            ServletContext servletContext, HttpServletRequest request,
350                            ThemeDisplay themeDisplay, Theme theme)
351                    throws Exception {
352    
353                    if (themeDisplay != null) {
354                            return themeDisplay.getPathThemeCss();
355                    }
356    
357                    if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
358                            String cdnHost = PortalUtil.getCDNHost(request);
359    
360                            if (Validator.isNotNull(cdnHost)) {
361                                    return cdnHost.concat(theme.getStaticResourcePath()).concat(
362                                            theme.getCssPath());
363                            }
364                    }
365    
366                    return servletContext.getRealPath(theme.getCssPath());
367            }
368    
369            private static URL _getRtlCustomResourceURL(
370                            ServletContext servletContext, String resourcePath)
371                    throws Exception {
372    
373                    return servletContext.getResource(
374                            CSSBuilderUtil.getRtlCustomFileName(resourcePath));
375            }
376    
377            private static File _getSassTempDir(ServletContext servletContext) {
378                    File sassTempDir = (File)servletContext.getAttribute(_SASS_DIR_KEY);
379    
380                    if (sassTempDir != null) {
381                            return sassTempDir;
382                    }
383    
384                    File tempDir = (File)servletContext.getAttribute(
385                            JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
386    
387                    sassTempDir = new File(tempDir, _SASS_DIR);
388    
389                    sassTempDir.mkdirs();
390    
391                    servletContext.setAttribute(_SASS_DIR_KEY, sassTempDir);
392    
393                    return sassTempDir;
394            }
395    
396            private static Theme _getTheme(HttpServletRequest request)
397                    throws Exception {
398    
399                    long companyId = PortalUtil.getCompanyId(request);
400    
401                    String themeId = ParamUtil.getString(request, "themeId");
402    
403                    if (Validator.isNotNull(themeId)) {
404                            try {
405                                    Theme theme = ThemeLocalServiceUtil.getTheme(
406                                            companyId, themeId, false);
407    
408                                    return theme;
409                            }
410                            catch (Exception e) {
411                                    _log.error(e, e);
412                            }
413                    }
414    
415                    String requestURI = URLDecoder.decode(
416                            request.getRequestURI(), StringPool.UTF8);
417    
418                    Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
419    
420                    if (portalThemeMatcher.find()) {
421                            String themePathId = portalThemeMatcher.group(1);
422    
423                            themePathId = StringUtil.replace(
424                                    themePathId, StringPool.UNDERLINE, StringPool.BLANK);
425    
426                            themeId = PortalUtil.getJsSafePortletId(themePathId);
427                    }
428                    else {
429                            Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
430                                    requestURI);
431    
432                            if (pluginThemeMatcher.find()) {
433                                    String themePathId = pluginThemeMatcher.group(1);
434    
435                                    themePathId = StringUtil.replace(
436                                            themePathId, StringPool.UNDERLINE, StringPool.BLANK);
437    
438                                    StringBundler sb = new StringBundler(4);
439    
440                                    sb.append(themePathId);
441                                    sb.append(PortletConstants.WAR_SEPARATOR);
442                                    sb.append(themePathId);
443                                    sb.append("theme");
444    
445                                    themePathId = sb.toString();
446    
447                                    themeId = PortalUtil.getJsSafePortletId(themePathId);
448                            }
449                    }
450    
451                    if (Validator.isNull(themeId)) {
452                            return null;
453                    }
454    
455                    try {
456                            Theme theme = ThemeLocalServiceUtil.getTheme(
457                                    companyId, themeId, false);
458    
459                            return theme;
460                    }
461                    catch (Exception e) {
462                            _log.error(e, e);
463                    }
464    
465                    return null;
466            }
467    
468            private static String _getThemeImagesPath(
469                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
470                    throws Exception {
471    
472                    String themeImagesPath = null;
473    
474                    if (themeDisplay != null) {
475                            themeImagesPath = themeDisplay.getPathThemeImages();
476                    }
477                    else {
478                            String cdnHost = PortalUtil.getCDNHost(request);
479                            String themeStaticResourcePath = theme.getStaticResourcePath();
480    
481                            themeImagesPath =
482                                    cdnHost + themeStaticResourcePath + theme.getImagesPath();
483                    }
484    
485                    return themeImagesPath;
486            }
487    
488            private static boolean _isImportsOnly(String content) {
489                    int pos = 0;
490    
491                    while (true) {
492                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
493                            int importY = content.indexOf(
494                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
495    
496                            if ((importX == -1) || (importY == -1)) {
497                                    String substring = content.substring(pos);
498    
499                                    substring = substring.trim();
500    
501                                    if (substring.isEmpty()) {
502                                            return true;
503                                    }
504                                    else {
505                                            return false;
506                                    }
507                            }
508    
509                            String substring = content.substring(pos, importX);
510    
511                            substring = substring.trim();
512    
513                            if (!substring.isEmpty()) {
514                                    return false;
515                            }
516    
517                            pos = importY + _CSS_IMPORT_END.length();
518                    }
519            }
520    
521            private static boolean _isThemeCssFastLoad(
522                    HttpServletRequest request, ThemeDisplay themeDisplay) {
523    
524                    if (!PropsValues.THEME_CSS_FAST_LOAD_CHECK_REQUEST_PARAMETER) {
525                            return PropsValues.THEME_CSS_FAST_LOAD;
526                    }
527    
528                    if (themeDisplay != null) {
529                            return themeDisplay.isThemeCssFastLoad();
530                    }
531    
532                    return SessionParamUtil.getBoolean(
533                            request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
534            }
535    
536            private static String _parseSass(
537                            ServletContext servletContext, HttpServletRequest request,
538                            ThemeDisplay themeDisplay, Theme theme, String content)
539                    throws Exception {
540    
541                    String portalWebDir = PortalUtil.getPortalWebDir();
542    
543                    String commonSassPath = portalWebDir.concat(_SASS_COMMON_DIR);
544                    String cssThemePath = _getCssThemePath(
545                            servletContext, request, themeDisplay, theme);
546    
547                    if (ServerDetector.isWebLogic() && !FileUtil.exists(commonSassPath)) {
548                            int pos = cssThemePath.indexOf("autodeploy/");
549    
550                            if (pos == -1) {
551                                    if (_log.isWarnEnabled()) {
552                                            _log.warn("Dynamic CSS compilation may not work");
553                                    }
554                            }
555                            else {
556                                    commonSassPath =
557                                            cssThemePath.substring(0, pos + 11) + "ROOT/" +
558                                                    _SASS_COMMON_DIR;
559                            }
560                    }
561    
562                    try {
563                            content = _sassCompiler.compileString(
564                                    content, commonSassPath + File.pathSeparator + cssThemePath,
565                                    "");
566                    }
567                    catch (Exception e) {
568                            _log.error(e, e);
569                    }
570    
571                    return content;
572            }
573    
574            private static final String _CSS_IMPORT_BEGIN = "@import url(";
575    
576            private static final String _CSS_IMPORT_END = ");";
577    
578            private static final String _SASS_COMMON_DIR = "/html/css/common";
579    
580            private static final String _SASS_DIR = "sass";
581    
582            private static final String _SASS_DIR_KEY =
583                    DynamicCSSUtil.class.getName() + "#sass";
584    
585            private static final Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
586    
587            private static boolean _initialized;
588            private static final Pattern _pluginThemePattern = Pattern.compile(
589                    "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
590            private static final Pattern _portalThemePattern = Pattern.compile(
591                    "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
592            private static SassCompiler _sassCompiler;
593    
594    }