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