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