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