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