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