001    /**
002     * Copyright (c) 2000-2012 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.strip;
016    
017    import com.liferay.portal.kernel.concurrent.ConcurrentLRUCache;
018    import com.liferay.portal.kernel.io.OutputStreamWriter;
019    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.portlet.LiferayWindowState;
023    import com.liferay.portal.kernel.scripting.ScriptingException;
024    import com.liferay.portal.kernel.servlet.HttpHeaders;
025    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
026    import com.liferay.portal.kernel.servlet.StringServletResponse;
027    import com.liferay.portal.kernel.util.CharPool;
028    import com.liferay.portal.kernel.util.ContentTypes;
029    import com.liferay.portal.kernel.util.GetterUtil;
030    import com.liferay.portal.kernel.util.HttpUtil;
031    import com.liferay.portal.kernel.util.JavaConstants;
032    import com.liferay.portal.kernel.util.KMPSearch;
033    import com.liferay.portal.kernel.util.ParamUtil;
034    import com.liferay.portal.kernel.util.Validator;
035    import com.liferay.portal.servlet.filters.BasePortalFilter;
036    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
037    import com.liferay.portal.util.MinifierUtil;
038    import com.liferay.portal.util.PropsValues;
039    
040    import java.io.Writer;
041    
042    import java.nio.CharBuffer;
043    
044    import java.util.HashSet;
045    import java.util.Set;
046    
047    import javax.servlet.FilterChain;
048    import javax.servlet.FilterConfig;
049    import javax.servlet.http.HttpServletRequest;
050    import javax.servlet.http.HttpServletResponse;
051    
052    /**
053     * @author Brian Wing Shun Chan
054     * @author Raymond Augé
055     * @author Shuyang Zhou
056     */
057    public class StripFilter extends BasePortalFilter {
058    
059            public static final String SKIP_FILTER =
060                    StripFilter.class.getName() + "SKIP_FILTER";
061    
062            public StripFilter() {
063                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
064                            _minifierCache = new ConcurrentLRUCache<String, String>(
065                                    PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE);
066                    }
067            }
068    
069            @Override
070            public void init(FilterConfig filterConfig) {
071                    super.init(filterConfig);
072    
073                    for (String ignorePath : PropsValues.STRIP_IGNORE_PATHS) {
074                            _ignorePaths.add(ignorePath);
075                    }
076            }
077    
078            @Override
079            public boolean isFilterEnabled(
080                    HttpServletRequest request, HttpServletResponse response) {
081    
082                    if (isStrip(request) && !isInclude(request) &&
083                            !isAlreadyFiltered(request)) {
084    
085                            return true;
086                    }
087                    else {
088                            return false;
089                    }
090            }
091    
092            protected String extractContent(CharBuffer charBuffer, int length) {
093    
094                    // See LPS-10545
095    
096                    /*String content = charBuffer.subSequence(0, length).toString();
097    
098                    int position = charBuffer.position();
099    
100                    charBuffer.position(position + length);*/
101    
102                    CharBuffer duplicateCharBuffer = charBuffer.duplicate();
103    
104                    int position = duplicateCharBuffer.position() + length;
105    
106                    String content = duplicateCharBuffer.limit(position).toString();
107    
108                    charBuffer.position(position);
109    
110                    return content;
111            }
112    
113            protected boolean hasMarker(CharBuffer charBuffer, char[] marker) {
114                    int position = charBuffer.position();
115    
116                    if ((position + marker.length) >= charBuffer.limit()) {
117                            return false;
118                    }
119    
120                    for (int i = 0; i < marker.length; i++) {
121                            char c = marker[i];
122    
123                            char oldC = charBuffer.charAt(i);
124    
125                            if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
126                                    return false;
127                            }
128                    }
129    
130                    return true;
131            }
132    
133            protected boolean isAlreadyFiltered(HttpServletRequest request) {
134                    if (request.getAttribute(SKIP_FILTER) != null) {
135                            return true;
136                    }
137                    else {
138                            return false;
139                    }
140            }
141    
142            protected boolean isInclude(HttpServletRequest request) {
143                    String uri = (String)request.getAttribute(
144                            JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
145    
146                    if (uri == null) {
147                            return false;
148                    }
149                    else {
150                            return true;
151                    }
152            }
153    
154            protected boolean isStrip(HttpServletRequest request) {
155                    if (!ParamUtil.getBoolean(request, _STRIP, true)) {
156                            return false;
157                    }
158    
159                    String path = request.getPathInfo();
160    
161                    if (_ignorePaths.contains(path)) {
162                            if (_log.isDebugEnabled()) {
163                                    _log.debug("Ignore path " + path);
164                            }
165    
166                            return false;
167                    }
168    
169                    // Modifying binary content through a servlet filter under certain
170                    // conditions is bad on performance the user will not start downloading
171                    // the content until the entire content is modified.
172    
173                    String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
174    
175                    if ((lifecycle.equals("1") &&
176                             LiferayWindowState.isExclusive(request)) ||
177                            lifecycle.equals("2")) {
178    
179                            return false;
180                    }
181                    else {
182                            return true;
183                    }
184            }
185    
186            protected void outputCloseTag(
187                            CharBuffer charBuffer, Writer writer, String closeTag)
188                    throws Exception {
189    
190                    writer.write(closeTag);
191    
192                    charBuffer.position(charBuffer.position() + closeTag.length());
193    
194                    skipWhiteSpace(charBuffer, writer, true);
195            }
196    
197            protected void outputOpenTag(
198                            CharBuffer charBuffer, Writer writer, char[] openTag)
199                    throws Exception {
200    
201                    writer.write(openTag);
202    
203                    charBuffer.position(charBuffer.position() + openTag.length);
204            }
205    
206            protected void processCSS(
207                            HttpServletRequest request, HttpServletResponse response,
208                            CharBuffer charBuffer, Writer writer)
209                    throws Exception {
210    
211                    outputOpenTag(charBuffer, writer, _MARKER_STYLE_OPEN);
212    
213                    int length = KMPSearch.search(
214                            charBuffer, _MARKER_STYLE_CLOSE, _MARKER_STYLE_CLOSE_NEXTS);
215    
216                    if (length == -1) {
217                            if (_log.isWarnEnabled()) {
218                                    _log.warn("Missing </style>");
219                            }
220    
221                            return;
222                    }
223    
224                    if (length == 0) {
225                            outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
226    
227                            return;
228                    }
229    
230                    String content = extractContent(charBuffer, length);
231    
232                    String minifiedContent = content;
233    
234                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
235                            String key = String.valueOf(content.hashCode());
236    
237                            minifiedContent = _minifierCache.get(key);
238    
239                            if (minifiedContent == null) {
240                                    if (PropsValues.STRIP_CSS_SASS_ENABLED) {
241                                            try {
242                                                    content = DynamicCSSUtil.parseSass(
243                                                            request, key, content);
244                                            }
245                                            catch (ScriptingException se) {
246                                                    _log.error("Unable to parse SASS on CSS " + key, se);
247    
248                                                    if (_log.isDebugEnabled()) {
249                                                            _log.debug(content);
250                                                    }
251    
252                                                    if (response != null) {
253                                                            response.setHeader(
254                                                                    HttpHeaders.CACHE_CONTROL,
255                                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
256                                                    }
257                                            }
258                                    }
259    
260                                    minifiedContent = MinifierUtil.minifyCss(content);
261    
262                                    boolean skipCache = false;
263    
264                                    for (String skipCss :
265                                                    PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SKIP_CSS) {
266    
267                                            if (minifiedContent.contains(skipCss)) {
268                                                    skipCache = true;
269    
270                                                    break;
271                                            }
272                                    }
273    
274                                    if (!skipCache) {
275                                            _minifierCache.put(key, minifiedContent);
276                                    }
277                            }
278                    }
279    
280                    if (!Validator.isNull(minifiedContent)) {
281                            writer.write(minifiedContent);
282                    }
283    
284                    outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
285            }
286    
287            @Override
288            protected void processFilter(
289                            HttpServletRequest request, HttpServletResponse response,
290                            FilterChain filterChain)
291                    throws Exception {
292    
293                    if (_log.isDebugEnabled()) {
294                            String completeURL = HttpUtil.getCompleteURL(request);
295    
296                            _log.debug("Stripping " + completeURL);
297                    }
298    
299                    request.setAttribute(SKIP_FILTER, Boolean.TRUE);
300    
301                    StringServletResponse stringResponse = new StringServletResponse(
302                            response);
303    
304                    processFilter(StripFilter.class, request, stringResponse, filterChain);
305    
306                    String contentType = GetterUtil.getString(
307                            stringResponse.getContentType()).toLowerCase();
308    
309                    if (_log.isDebugEnabled()) {
310                            _log.debug("Stripping content of type " + contentType);
311                    }
312    
313                    response.setContentType(contentType);
314    
315                    if (contentType.startsWith(ContentTypes.TEXT_HTML) &&
316                            (stringResponse.getStatus() == HttpServletResponse.SC_OK)) {
317    
318                            CharBuffer oldCharBuffer = CharBuffer.wrap(
319                                    stringResponse.getString());
320    
321                            boolean ensureContentLength = ParamUtil.getBoolean(
322                                    request, _ENSURE_CONTENT_LENGTH);
323    
324                            if (ensureContentLength) {
325                                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
326                                            new UnsyncByteArrayOutputStream();
327    
328                                    strip(
329                                            request, response, oldCharBuffer,
330                                            new OutputStreamWriter(unsyncByteArrayOutputStream));
331    
332                                    response.setContentLength(unsyncByteArrayOutputStream.size());
333    
334                                    unsyncByteArrayOutputStream.writeTo(response.getOutputStream());
335                            }
336                            else {
337                                    strip(request, response, oldCharBuffer, response.getWriter());
338                            }
339                    }
340                    else {
341                            ServletResponseUtil.write(response, stringResponse);
342                    }
343            }
344    
345            protected void processInput(CharBuffer oldCharBuffer, Writer writer)
346                    throws Exception {
347    
348                    int length = KMPSearch.search(
349                            oldCharBuffer, _MARKER_INPUT_OPEN.length + 1, _MARKER_INPUT_CLOSE,
350                            _MARKER_INPUT_CLOSE_NEXTS);
351    
352                    if (length == -1) {
353                            if (_log.isWarnEnabled()) {
354                                    _log.warn("Missing />");
355                            }
356    
357                            outputOpenTag(oldCharBuffer, writer, _MARKER_INPUT_OPEN);
358    
359                            return;
360                    }
361    
362                    length += _MARKER_INPUT_CLOSE.length();
363    
364                    String content = extractContent(oldCharBuffer, length);
365    
366                    writer.write(content);
367    
368                    skipWhiteSpace(oldCharBuffer, writer, true);
369            }
370    
371            protected void processJavaScript(
372                            CharBuffer charBuffer, Writer writer, char[] openTag)
373                    throws Exception {
374    
375                    int endPos = openTag.length + 1;
376    
377                    char c = charBuffer.charAt(openTag.length);
378    
379                    if (c == CharPool.SPACE) {
380                            int startPos = openTag.length + 1;
381    
382                            for (int i = startPos; i < charBuffer.length(); i++) {
383                                    c = charBuffer.charAt(i);
384    
385                                    if (c == CharPool.GREATER_THAN) {
386    
387                                            // Open script tag complete
388    
389                                            endPos = i + 1;
390    
391                                            int length = i - startPos;
392    
393                                            if ((length < _MARKER_TYPE_JAVASCRIPT.length()) ||
394                                                    (KMPSearch.search(
395                                                            charBuffer, startPos, length,
396                                                            _MARKER_TYPE_JAVASCRIPT,
397                                                            _MARKER_TYPE_JAVASCRIPT_NEXTS) == -1)) {
398    
399                                                    // Open script tag has attribute other than
400                                                    // type="text/javascript". Skip stripping.
401    
402                                                    return;
403                                            }
404    
405                                            // Open script tag has no attribute or has attribute
406                                            // type="text/javascript". Start stripping.
407    
408                                            break;
409                                    }
410                                    else if (c == CharPool.LESS_THAN) {
411    
412                                            // Illegal open script tag. Found a '<' before seeing a '>'.
413    
414                                            return;
415                                    }
416                            }
417    
418                            if (endPos == charBuffer.length()) {
419    
420                                    // Illegal open script tag. Unable to find a '>'.
421    
422                                    return;
423                            }
424                    }
425                    else if (c != CharPool.GREATER_THAN) {
426    
427                            // Illegal open script tag. Not followed by a '>' or a ' '.
428    
429                            return;
430                    }
431    
432                    writer.append(charBuffer, 0, endPos);
433    
434                    charBuffer.position(charBuffer.position() + endPos);
435    
436                    int length = KMPSearch.search(
437                            charBuffer, _MARKER_SCRIPT_CLOSE, _MARKER_SCRIPT_CLOSE_NEXTS);
438    
439                    if (length == -1) {
440                            if (_log.isWarnEnabled()) {
441                                    _log.warn("Missing </script>");
442                            }
443    
444                            return;
445                    }
446    
447                    if (length == 0) {
448                            outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
449    
450                            return;
451                    }
452    
453                    String content = extractContent(charBuffer, length);
454    
455                    String minifiedContent = content;
456    
457                    if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
458                            String key = String.valueOf(content.hashCode());
459    
460                            minifiedContent = _minifierCache.get(key);
461    
462                            if (minifiedContent == null) {
463                                    minifiedContent = MinifierUtil.minifyJavaScript(content);
464    
465                                    boolean skipCache = false;
466    
467                                    for (String skipJavaScript :
468                                                    PropsValues.
469                                                            MINIFIER_INLINE_CONTENT_CACHE_SKIP_JAVASCRIPT) {
470    
471                                            if (minifiedContent.contains(skipJavaScript)) {
472                                                    skipCache = true;
473    
474                                                    break;
475                                            }
476                                    }
477    
478                                    if (!skipCache) {
479                                            _minifierCache.put(key, minifiedContent);
480                                    }
481                            }
482                    }
483    
484                    if (!Validator.isNull(minifiedContent)) {
485                            writer.write(_CDATA_OPEN);
486                            writer.write(minifiedContent);
487                            writer.write(_CDATA_CLOSE);
488                    }
489    
490                    outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
491            }
492    
493            protected void processPre(CharBuffer oldCharBuffer, Writer writer)
494                    throws Exception {
495    
496                    int length = KMPSearch.search(
497                            oldCharBuffer, _MARKER_PRE_OPEN.length + 1, _MARKER_PRE_CLOSE,
498                            _MARKER_PRE_CLOSE_NEXTS);
499    
500                    if (length == -1) {
501                            if (_log.isWarnEnabled()) {
502                                    _log.warn("Missing </pre>");
503                            }
504    
505                            outputOpenTag(oldCharBuffer, writer, _MARKER_PRE_OPEN);
506    
507                            return;
508                    }
509    
510                    length += _MARKER_PRE_CLOSE.length();
511    
512                    String content = extractContent(oldCharBuffer, length);
513    
514                    writer.write(content);
515    
516                    skipWhiteSpace(oldCharBuffer, writer, true);
517            }
518    
519            protected void processTextArea(CharBuffer oldCharBuffer, Writer writer)
520                    throws Exception {
521    
522                    int length = KMPSearch.search(
523                            oldCharBuffer, _MARKER_TEXTAREA_OPEN.length + 1,
524                            _MARKER_TEXTAREA_CLOSE, _MARKER_TEXTAREA_CLOSE_NEXTS);
525    
526                    if (length == -1) {
527                            if (_log.isWarnEnabled()) {
528                                    _log.warn("Missing </textArea>");
529                            }
530    
531                            outputOpenTag(oldCharBuffer, writer, _MARKER_TEXTAREA_OPEN);
532                            return;
533                    }
534    
535                    length += _MARKER_TEXTAREA_CLOSE.length();
536    
537                    String content = extractContent(oldCharBuffer, length);
538    
539                    writer.write(content);
540    
541                    skipWhiteSpace(oldCharBuffer, writer, true);
542            }
543    
544            protected boolean skipWhiteSpace(
545                            CharBuffer charBuffer, Writer writer, boolean appendSeparator)
546                    throws Exception {
547    
548                    boolean skipped = false;
549    
550                    for (int i = charBuffer.position(); i < charBuffer.limit(); i++) {
551                            char c = charBuffer.get();
552    
553                            if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
554                                    (c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
555    
556                                    skipped = true;
557    
558                                    continue;
559                            }
560                            else {
561                                    charBuffer.position(i);
562    
563                                    break;
564                            }
565                    }
566    
567                    if (skipped && appendSeparator) {
568                            writer.write(CharPool.SPACE);
569                    }
570    
571                    return skipped;
572            }
573    
574            protected void strip(
575                            HttpServletRequest request, HttpServletResponse response,
576                            CharBuffer charBuffer, Writer writer)
577                    throws Exception {
578    
579                    skipWhiteSpace(charBuffer, writer, false);
580    
581                    while (charBuffer.hasRemaining()) {
582                            char c = charBuffer.get();
583    
584                            writer.write(c);
585    
586                            if (c == CharPool.LESS_THAN) {
587                                    if (hasMarker(charBuffer, _MARKER_INPUT_OPEN)) {
588                                            processInput(charBuffer, writer);
589    
590                                            continue;
591                                    }
592                                    else if (hasMarker(charBuffer, _MARKER_PRE_OPEN)) {
593                                            processPre(charBuffer, writer);
594    
595                                            continue;
596                                    }
597                                    else if (hasMarker(charBuffer, _MARKER_TEXTAREA_OPEN)) {
598                                            processTextArea(charBuffer, writer);
599    
600                                            continue;
601                                    }
602                                    else if (hasMarker(charBuffer, _MARKER_SCRIPT_OPEN)) {
603                                            processJavaScript(charBuffer, writer, _MARKER_SCRIPT_OPEN);
604    
605                                            continue;
606                                    }
607                                    else if (hasMarker(charBuffer, _MARKER_STYLE_OPEN)) {
608                                            processCSS(request, response, charBuffer, writer);
609    
610                                            continue;
611                                    }
612                            }
613                            else if (c == CharPool.GREATER_THAN) {
614                                    skipWhiteSpace(charBuffer, writer, true);
615                            }
616    
617                            skipWhiteSpace(charBuffer, writer, true);
618                    }
619    
620                    writer.flush();
621            }
622    
623            private static final String _CDATA_CLOSE = "/*]]>*/";
624    
625            private static final String _CDATA_OPEN = "/*<![CDATA[*/";
626    
627            private static final String _ENSURE_CONTENT_LENGTH = "ensureContentLength";
628    
629            private static final String _MARKER_INPUT_CLOSE = "/>";
630    
631            private static final int[] _MARKER_INPUT_CLOSE_NEXTS =
632                    KMPSearch.generateNexts(_MARKER_INPUT_CLOSE);
633    
634            private static final char[] _MARKER_INPUT_OPEN = "input".toCharArray();
635    
636            private static final String _MARKER_PRE_CLOSE = "/pre>";
637    
638            private static final int[] _MARKER_PRE_CLOSE_NEXTS =
639                    KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
640    
641            private static final char[] _MARKER_PRE_OPEN = "pre".toCharArray();
642    
643            private static final String _MARKER_SCRIPT_CLOSE = "</script>";
644    
645            private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
646                    KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
647    
648            private static final char[] _MARKER_SCRIPT_OPEN = "script".toCharArray();
649    
650            private static final String _MARKER_STYLE_CLOSE = "</style>";
651    
652            private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
653                    KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
654    
655            private static final char[] _MARKER_STYLE_OPEN =
656                    "style type=\"text/css\">".toCharArray();
657    
658            private static final String _MARKER_TEXTAREA_CLOSE = "/textarea>";
659    
660            private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
661                    KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
662    
663            private static final char[] _MARKER_TEXTAREA_OPEN =
664                    "textarea ".toCharArray();
665    
666            private static final String _MARKER_TYPE_JAVASCRIPT =
667                    "type=\"text/javascript\"";
668    
669            private static final int[] _MARKER_TYPE_JAVASCRIPT_NEXTS =
670                    KMPSearch.generateNexts(_MARKER_TYPE_JAVASCRIPT);
671    
672            private static final String _STRIP = "strip";
673    
674            private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
675    
676            private Set<String> _ignorePaths = new HashSet<String>();
677            private ConcurrentLRUCache<String, String> _minifierCache;
678    
679    }