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