1   /**
2    * Copyright (c) 2000-2010 Liferay, Inc. All rights reserved.
3    *
4    * This library is free software; you can redistribute it and/or modify it under
5    * the terms of the GNU Lesser General Public License as published by the Free
6    * Software Foundation; either version 2.1 of the License, or (at your option)
7    * any later version.
8    *
9    * This library is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11   * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12   * details.
13   */
14  
15  package com.liferay.portal.servlet.filters.strip;
16  
17  import com.liferay.portal.kernel.concurrent.ConcurrentLRUCache;
18  import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
19  import com.liferay.portal.kernel.log.Log;
20  import com.liferay.portal.kernel.log.LogFactoryUtil;
21  import com.liferay.portal.kernel.nio.charset.CharsetEncoderUtil;
22  import com.liferay.portal.kernel.portlet.LiferayWindowState;
23  import com.liferay.portal.kernel.servlet.StringServletResponse;
24  import com.liferay.portal.kernel.util.CharPool;
25  import com.liferay.portal.kernel.util.GetterUtil;
26  import com.liferay.portal.kernel.util.HttpUtil;
27  import com.liferay.portal.kernel.util.JavaConstants;
28  import com.liferay.portal.kernel.util.KMPSearch;
29  import com.liferay.portal.kernel.util.ParamUtil;
30  import com.liferay.portal.kernel.util.StringPool;
31  import com.liferay.portal.kernel.util.Validator;
32  import com.liferay.portal.servlet.filters.BasePortalFilter;
33  import com.liferay.portal.util.MinifierUtil;
34  import com.liferay.portal.util.PropsValues;
35  import com.liferay.util.servlet.ServletResponseUtil;
36  
37  import java.io.IOException;
38  import java.io.OutputStream;
39  
40  import java.nio.ByteBuffer;
41  
42  import java.util.HashSet;
43  import java.util.Set;
44  
45  import javax.servlet.FilterChain;
46  import javax.servlet.FilterConfig;
47  import javax.servlet.http.HttpServletRequest;
48  import javax.servlet.http.HttpServletResponse;
49  
50  /**
51   * <a href="StripFilter.java.html"><b><i>View Source</i></b></a>
52   *
53   * @author Brian Wing Shun Chan
54   * @author Raymond Augé
55   * @author Shuyang Zhou
56   */
57  public class StripFilter extends BasePortalFilter {
58  
59      public static final String SKIP_FILTER =
60          StripFilter.class.getName() + "SKIP_FILTER";
61  
62      public void init(FilterConfig filterConfig) {
63          super.init(filterConfig);
64  
65          for (String ignorePath : PropsValues.STRIP_IGNORE_PATHS) {
66              _ignorePaths.add(ignorePath);
67          }
68      }
69  
70      protected int countContinuousWhiteSpace(byte[] oldByteArray, int offset) {
71          int count = 0;
72  
73          for (int i = offset ; i < oldByteArray.length ; i++) {
74              char c = (char)oldByteArray[i];
75  
76              if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
77                  (c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
78  
79                  count++;
80              }
81              else{
82                  return count;
83              }
84          }
85  
86          return count;
87      }
88  
89      protected boolean hasMarker(byte[] oldByteArray, int pos, byte[] marker) {
90          if ((pos + marker.length) >= oldByteArray.length) {
91              return false;
92          }
93  
94          for (int i = 0; i < marker.length; i++) {
95              byte c = marker[i];
96  
97              byte oldC = oldByteArray[pos + i + 1];
98  
99              if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
100                 return false;
101             }
102         }
103 
104         return true;
105     }
106 
107     protected boolean isAlreadyFiltered(HttpServletRequest request) {
108         if (request.getAttribute(SKIP_FILTER) != null) {
109             return true;
110         }
111         else {
112             return false;
113         }
114     }
115 
116     protected boolean isInclude(HttpServletRequest request) {
117         String uri = (String)request.getAttribute(
118             JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
119 
120         if (uri == null) {
121             return false;
122         }
123         else {
124             return true;
125         }
126     }
127 
128     protected boolean isStrip(HttpServletRequest request) {
129         if (!ParamUtil.getBoolean(request, _STRIP, true)) {
130             return false;
131         }
132 
133         String path = request.getPathInfo();
134 
135         if (_ignorePaths.contains(path)) {
136             if (_log.isDebugEnabled()) {
137                 _log.debug("Ignore path " + path);
138             }
139 
140             return false;
141         }
142 
143         // Modifying binary content through a servlet filter under certain
144         // conditions is bad on performance the user will not start downloading
145         // the content until the entire content is modified.
146 
147         String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
148 
149         if ((lifecycle.equals("1") &&
150              LiferayWindowState.isExclusive(request)) ||
151             lifecycle.equals("2")) {
152 
153             return false;
154         }
155         else {
156             return true;
157         }
158     }
159 
160     protected int processCSS(
161             byte[] oldByteArray, OutputStream newBytes, int currentIndex)
162         throws IOException {
163 
164         int beginIndex = currentIndex + _MARKER_STYLE_OPEN.length + 1;
165 
166         int endIndex = KMPSearch.search(
167             oldByteArray, beginIndex, _MARKER_STYLE_CLOSE,
168             _MARKER_STYLE_CLOSE_NEXTS);
169 
170         if (endIndex == -1) {
171             _log.error("Missing </style>");
172 
173             return currentIndex + 1;
174         }
175 
176         int newBeginIndex = endIndex + _MARKER_STYLE_CLOSE.length;
177 
178         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
179 
180         String content = new String(
181             oldByteArray, beginIndex, endIndex - beginIndex);
182 
183         if (Validator.isNull(content)) {
184             return newBeginIndex;
185         }
186 
187         String minifiedContent = content;
188 
189         if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
190             String key = String.valueOf(content.hashCode());
191 
192             minifiedContent = _minifierCache.get(key);
193 
194             if (minifiedContent == null) {
195                 minifiedContent = MinifierUtil.minifyCss(content);
196 
197                 boolean skipCache = false;
198 
199                 for (String skipCss :
200                         PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SKIP_CSS) {
201 
202                     if (minifiedContent.contains(skipCss)) {
203                         skipCache = true;
204 
205                         break;
206                     }
207                 }
208 
209                 if (!skipCache) {
210                     _minifierCache.put(key, minifiedContent);
211                 }
212             }
213         }
214 
215         if (Validator.isNull(minifiedContent)) {
216             return newBeginIndex;
217         }
218 
219         ByteBuffer contentByteBuffer = CharsetEncoderUtil.encode(
220             StringPool.UTF8, minifiedContent);
221 
222         newBytes.write(_STYLE_TYPE_CSS);
223         newBytes.write(contentByteBuffer.array(), 0, contentByteBuffer.limit());
224         newBytes.write(_MARKER_STYLE_CLOSE);
225 
226         return newBeginIndex;
227     }
228 
229     protected void processFilter(
230             HttpServletRequest request, HttpServletResponse response,
231             FilterChain filterChain)
232         throws Exception {
233 
234         if (isStrip(request) && !isInclude(request) &&
235             !isAlreadyFiltered(request)) {
236 
237             if (_log.isDebugEnabled()) {
238                 String completeURL = HttpUtil.getCompleteURL(request);
239 
240                 _log.debug("Stripping " + completeURL);
241             }
242 
243             request.setAttribute(SKIP_FILTER, Boolean.TRUE);
244 
245             StringServletResponse stringResponse = new StringServletResponse(
246                 response);
247 
248             processFilter(
249                 StripFilter.class, request, stringResponse, filterChain);
250 
251             String contentType = GetterUtil.getString(
252                 stringResponse.getContentType()).toLowerCase();
253 
254             if (_log.isDebugEnabled()) {
255                 _log.debug("Stripping content of type " + contentType);
256             }
257 
258             response.setContentType(contentType);
259 
260             if (contentType.indexOf("text/") != -1) {
261                 byte[] oldByteArray = null;
262                 int length = 0;
263 
264                 if (stringResponse.isCalledGetOutputStream()) {
265                     UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
266                         stringResponse.getUnsyncByteArrayOutputStream();
267 
268                     oldByteArray =
269                         unsyncByteArrayOutputStream.unsafeGetByteArray();
270                     length = unsyncByteArrayOutputStream.size();
271                 }
272                 else {
273                     String content = stringResponse.getString();
274 
275                     ByteBuffer contentByteBuffer = CharsetEncoderUtil.encode(
276                         StringPool.UTF8, content);
277 
278                     oldByteArray = contentByteBuffer.array();
279                     length = contentByteBuffer.limit();
280                 }
281 
282                 UnsyncByteArrayOutputStream outputStream =
283                     new UnsyncByteArrayOutputStream(
284                         (int)(length * _COMPRESSION_RATE));
285 
286                 strip(oldByteArray, length, outputStream);
287 
288                 ServletResponseUtil.write(
289                     response, outputStream.unsafeGetByteArray(),
290                     outputStream.size());
291             }
292             else {
293                 ServletResponseUtil.write(response, stringResponse);
294             }
295         }
296         else {
297             if (_log.isDebugEnabled()) {
298                 String completeURL = HttpUtil.getCompleteURL(request);
299 
300                 _log.debug("Not stripping " + completeURL);
301             }
302 
303             processFilter(StripFilter.class, request, response, filterChain);
304         }
305     }
306 
307     protected int processJavaScript(
308             byte[] oldByteArray, OutputStream newBytes, int currentIndex,
309             byte[] openTag)
310         throws IOException {
311 
312         int beginIndex = currentIndex + openTag.length + 1;
313 
314         int endIndex = KMPSearch.search(
315             oldByteArray, beginIndex, _MARKER_SCRIPT_CLOSE,
316             _MARKER_SCRIPT_CLOSE_NEXTS);
317 
318         if (endIndex == -1) {
319             _log.error("Missing </script>");
320 
321             return currentIndex + 1;
322         }
323 
324         int newBeginIndex = endIndex + _MARKER_SCRIPT_CLOSE.length;
325 
326         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
327 
328         String content = new String(
329             oldByteArray, beginIndex, endIndex - beginIndex);
330 
331         if (Validator.isNull(content)) {
332             return newBeginIndex;
333         }
334 
335         String minifiedContent = content;
336 
337         if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
338             String key = String.valueOf(content.hashCode());
339 
340             minifiedContent = _minifierCache.get(key);
341 
342             if (minifiedContent == null) {
343                 minifiedContent = MinifierUtil.minifyJavaScript(content);
344 
345                 boolean skipCache = false;
346 
347                 for (String skipJavaScript :
348                         PropsValues.
349                             MINIFIER_INLINE_CONTENT_CACHE_SKIP_JAVASCRIPT) {
350 
351                     if (minifiedContent.contains(skipJavaScript)) {
352                         skipCache = true;
353 
354                         break;
355                     }
356                 }
357 
358                 if (!skipCache) {
359                     _minifierCache.put(key, minifiedContent);
360                 }
361             }
362         }
363 
364         if (Validator.isNull(minifiedContent)) {
365             return newBeginIndex;
366         }
367 
368         ByteBuffer contentByteBuffer = CharsetEncoderUtil.encode(
369             StringPool.UTF8, minifiedContent);
370 
371         newBytes.write(_SCRIPT_TYPE_JAVASCRIPT);
372         newBytes.write(_CDATA_OPEN);
373         newBytes.write(contentByteBuffer.array(), 0, contentByteBuffer.limit());
374         newBytes.write(_CDATA_CLOSE);
375         newBytes.write(_MARKER_SCRIPT_CLOSE);
376 
377         return newBeginIndex;
378     }
379 
380     protected int processPre(
381             byte[] oldByteArray, OutputStream newBytes, int currentIndex)
382         throws IOException {
383 
384         int beginIndex = currentIndex + _MARKER_PRE_OPEN.length + 1;
385 
386         int endIndex = KMPSearch.search(
387             oldByteArray, beginIndex, _MARKER_PRE_CLOSE,
388             _MARKER_PRE_CLOSE_NEXTS);
389 
390         if (endIndex == -1) {
391             _log.error("Missing </pre>");
392 
393             return currentIndex + 1;
394         }
395 
396         int newBeginIndex = endIndex + _MARKER_PRE_CLOSE.length;
397 
398         newBytes.write(
399             oldByteArray, currentIndex, newBeginIndex - currentIndex);
400 
401         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
402 
403         return newBeginIndex;
404     }
405 
406     protected int processTextArea(
407             byte[] oldByteArray, OutputStream newBytes, int currentIndex)
408         throws IOException {
409 
410         int beginIndex = currentIndex + _MARKER_TEXTAREA_OPEN.length + 1;
411 
412         int endIndex = KMPSearch.search(
413             oldByteArray, beginIndex, _MARKER_TEXTAREA_CLOSE,
414             _MARKER_TEXTAREA_CLOSE_NEXTS);
415 
416         if (endIndex == -1) {
417             _log.error("Missing </textArea>");
418 
419             return currentIndex + 1;
420         }
421 
422         int newBeginIndex = endIndex + _MARKER_TEXTAREA_CLOSE.length;
423 
424         newBytes.write(
425             oldByteArray, currentIndex, newBeginIndex - currentIndex);
426 
427         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
428 
429         return newBeginIndex;
430     }
431 
432     protected void strip(
433             byte[] oldByteArray, int length, OutputStream outputStream)
434         throws IOException {
435 
436         int count = countContinuousWhiteSpace(oldByteArray, 0);
437 
438         for (int i = count; i < length; i++) {
439             byte b = oldByteArray[i];
440 
441             if (b == CharPool.LESS_THAN) {
442                 if (hasMarker(oldByteArray, i, _MARKER_PRE_OPEN)) {
443                     i = processPre(oldByteArray, outputStream, i) - 1;
444 
445                     continue;
446                 }
447                 else if (hasMarker(oldByteArray, i, _MARKER_TEXTAREA_OPEN)) {
448                     i = processTextArea(oldByteArray, outputStream, i) - 1;
449 
450                     continue;
451                 }
452                 else if (hasMarker(oldByteArray, i, _MARKER_JS_OPEN)) {
453                     i = processJavaScript(
454                             oldByteArray, outputStream, i, _MARKER_JS_OPEN) - 1;
455 
456                     continue;
457                 }
458                 else if (hasMarker(oldByteArray, i, _MARKER_SCRIPT_OPEN)) {
459                     i = processJavaScript(
460                             oldByteArray, outputStream, i,
461                             _MARKER_SCRIPT_OPEN) - 1;
462 
463                     continue;
464                 }
465                 else if (hasMarker(oldByteArray, i, _MARKER_STYLE_OPEN)) {
466                     i = processCSS(oldByteArray, outputStream, i) - 1;
467 
468                     continue;
469                 }
470             }
471             else if (b == CharPool.GREATER_THAN) {
472                 outputStream.write(b);
473 
474                 int spaceCount = countContinuousWhiteSpace(oldByteArray, i + 1);
475 
476                 if (spaceCount > 0) {
477                     i = i + spaceCount;
478 
479                     outputStream.write(CharPool.SPACE);
480                 }
481 
482                 continue;
483             }
484 
485             int spaceCount = countContinuousWhiteSpace(oldByteArray, i);
486 
487             if (spaceCount > 0) {
488                 outputStream.write(CharPool.SPACE);
489 
490                 i = i + spaceCount - 1;
491             }
492             else {
493                 outputStream.write(b);
494             }
495         }
496 
497         outputStream.flush();
498     }
499 
500     private static final byte[] _CDATA_CLOSE = "/*]]>*/".getBytes();
501 
502     private static final byte[] _CDATA_OPEN = "/*<![CDATA[*/".getBytes();
503 
504     private static final double _COMPRESSION_RATE = 0.7;
505 
506     private static final byte[] _MARKER_JS_OPEN =
507         "script type=\"text/javascript\">".getBytes();
508 
509     private static final byte[] _MARKER_PRE_CLOSE = "/pre>".getBytes();
510 
511     private static final int[] _MARKER_PRE_CLOSE_NEXTS =
512         KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
513 
514     private static final byte[] _MARKER_PRE_OPEN = "pre>".getBytes();
515 
516     private static final byte[] _MARKER_SCRIPT_CLOSE = "</script>".getBytes();
517 
518     private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
519         KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
520 
521     private static final byte[] _MARKER_SCRIPT_OPEN = "script>".getBytes();
522 
523     private static final byte[] _MARKER_STYLE_CLOSE = "</style>".getBytes();
524 
525     private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
526         KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
527 
528     private static final byte[] _MARKER_STYLE_OPEN =
529         "style type=\"text/css\">".getBytes();
530 
531     private static final byte[] _MARKER_TEXTAREA_CLOSE =
532         "/textarea>".getBytes();
533 
534     private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
535         KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
536 
537     private static final byte[] _MARKER_TEXTAREA_OPEN =
538         "textarea ".getBytes();
539 
540     private static final byte[] _SCRIPT_TYPE_JAVASCRIPT =
541         "<script type=\"text/javascript\">".getBytes();
542 
543     private static final String _STRIP = "strip";
544 
545     private static final byte[] _STYLE_TYPE_CSS =
546         "<style type=\"text/css\">".getBytes();
547 
548     private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
549 
550     private ConcurrentLRUCache<String, String> _minifierCache =
551         new ConcurrentLRUCache<String, String>(
552             PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE);
553     private Set<String> _ignorePaths = new HashSet<String>();
554 
555 }