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