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