1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    *
5    *
6    *
7    * The contents of this file are subject to the terms of the Liferay Enterprise
8    * Subscription License ("License"). You may not use this file except in
9    * compliance with the License. You can obtain a copy of the License by
10   * contacting Liferay, Inc. See the License for the specific language governing
11   * permissions and limitations under the License, including but not limited to
12   * distribution rights of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.servlet.filters.strip;
24  
25  import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
26  import com.liferay.portal.kernel.log.Log;
27  import com.liferay.portal.kernel.log.LogFactoryUtil;
28  import com.liferay.portal.kernel.portlet.LiferayWindowState;
29  import com.liferay.portal.kernel.util.CharPool;
30  import com.liferay.portal.kernel.util.GetterUtil;
31  import com.liferay.portal.kernel.util.HttpUtil;
32  import com.liferay.portal.kernel.util.JavaConstants;
33  import com.liferay.portal.kernel.util.KMPSearch;
34  import com.liferay.portal.kernel.util.ParamUtil;
35  import com.liferay.portal.kernel.util.Validator;
36  import com.liferay.portal.servlet.filters.BasePortalFilter;
37  import com.liferay.portal.servlet.filters.etag.ETagUtil;
38  import com.liferay.portal.util.MinifierUtil;
39  import com.liferay.util.servlet.ServletResponseUtil;
40  
41  import javax.servlet.FilterChain;
42  import javax.servlet.http.HttpServletRequest;
43  import javax.servlet.http.HttpServletResponse;
44  
45  /**
46   * <a href="StripFilter.java.html"><b><i>View Source</i></b></a>
47   *
48   * @author Brian Wing Shun Chan
49   * @author Raymond Augé
50   * @author Shuyang Zhou
51   */
52  public class StripFilter extends BasePortalFilter {
53  
54      public static final String SKIP_FILTER =
55          StripFilter.class.getName() + "SKIP_FILTER";
56  
57      protected int countContinuousWhiteSpace(byte[] oldByteArray, int offset) {
58          int count = 0;
59  
60          for (int i = offset ; i < oldByteArray.length ; i++) {
61              char c = (char)oldByteArray[i];
62  
63              if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
64                  (c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
65  
66                  count++;
67              }
68              else{
69                  return count;
70              }
71          }
72  
73          return count;
74      }
75  
76      protected boolean hasMarker(byte[] oldByteArray, int pos, byte[] marker) {
77          if ((pos + marker.length) >= oldByteArray.length) {
78              return false;
79          }
80  
81          for (int i = 0; i < marker.length; i++) {
82              byte c = marker[i];
83  
84              byte oldC = oldByteArray[pos + i + 1];
85  
86              if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
87                  return false;
88              }
89          }
90  
91          return true;
92      }
93  
94      protected boolean isAlreadyFiltered(HttpServletRequest request) {
95          if (request.getAttribute(SKIP_FILTER) != null) {
96              return true;
97          }
98          else {
99              return false;
100         }
101     }
102 
103     protected boolean isInclude(HttpServletRequest request) {
104         String uri = (String)request.getAttribute(
105             JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
106 
107         if (uri == null) {
108             return false;
109         }
110         else {
111             return true;
112         }
113     }
114 
115     protected boolean isStrip(HttpServletRequest request) {
116         if (!ParamUtil.getBoolean(request, _STRIP, true)) {
117             return false;
118         }
119         else {
120 
121             // Modifying binary content through a servlet filter under certain
122             // conditions is bad on performance the user will not start
123             // downloading the content until the entire content is modified.
124 
125             String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
126 
127             if ((lifecycle.equals("1") &&
128                  LiferayWindowState.isExclusive(request)) ||
129                 lifecycle.equals("2")) {
130 
131                 return false;
132             }
133             else {
134                 return true;
135             }
136         }
137     }
138 
139     protected int processCSS(
140         byte[] oldByteArray, UnsyncByteArrayOutputStream newBytes,
141         int currentIndex) {
142 
143         int beginIndex = currentIndex + _MARKER_STYLE_OPEN.length + 1;
144 
145         int endIndex = KMPSearch.search(
146             oldByteArray, beginIndex, _MARKER_STYLE_CLOSE,
147             _MARKER_STYLE_CLOSE_NEXTS);
148 
149         if (endIndex == -1) {
150             _log.error("Missing </style>");
151 
152             return currentIndex + 1;
153         }
154 
155         int newBeginIndex = endIndex + _MARKER_STYLE_CLOSE.length;
156 
157         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
158 
159         String content = new String(
160             oldByteArray, beginIndex, endIndex - beginIndex);
161 
162         if (Validator.isNull(content)) {
163             return newBeginIndex;
164         }
165 
166         content = MinifierUtil.minifyCss(content);
167 
168         if (Validator.isNull(content)) {
169             return newBeginIndex;
170         }
171 
172         newBytes.write(_STYLE_TYPE_CSS);
173         newBytes.write(content.getBytes());
174         newBytes.write(_MARKER_STYLE_CLOSE);
175 
176         return newBeginIndex;
177     }
178 
179     protected void processFilter(
180             HttpServletRequest request, HttpServletResponse response,
181             FilterChain filterChain)
182         throws Exception {
183 
184         if (isStrip(request) && !isInclude(request) &&
185             !isAlreadyFiltered(request)) {
186 
187             if (_log.isDebugEnabled()) {
188                 String completeURL = HttpUtil.getCompleteURL(request);
189 
190                 _log.debug("Stripping " + completeURL);
191             }
192 
193             request.setAttribute(SKIP_FILTER, Boolean.TRUE);
194 
195             StripResponse stripResponse = new StripResponse(response);
196 
197             processFilter(
198                 StripFilter.class, request, stripResponse, filterChain);
199 
200             String contentType = GetterUtil.getString(
201                 stripResponse.getContentType()).toLowerCase();
202 
203             byte[] oldByteArray = stripResponse.getData();
204 
205             if ((oldByteArray != null) && (oldByteArray.length > 0)) {
206                 byte[] newByteArray = null;
207 
208                 if (_log.isDebugEnabled()) {
209                     _log.debug("Stripping content of type " + contentType);
210                 }
211 
212                 if (contentType.indexOf("text/") != -1) {
213                     newByteArray = strip(oldByteArray);
214                 }
215                 else {
216                     newByteArray = oldByteArray;
217                 }
218 
219                 if (!ETagUtil.processETag(request, response, newByteArray)) {
220                     response.setContentType(contentType);
221 
222                     ServletResponseUtil.write(response, newByteArray);
223                 }
224             }
225         }
226         else {
227             if (_log.isDebugEnabled()) {
228                 String completeURL = HttpUtil.getCompleteURL(request);
229 
230                 _log.debug("Not stripping " + completeURL);
231             }
232 
233             processFilter(StripFilter.class, request, response, filterChain);
234         }
235     }
236 
237     protected int processJavaScript(
238         byte[] oldByteArray, UnsyncByteArrayOutputStream newBytes,
239         int currentIndex, byte[] openTag) {
240 
241         int beginIndex = currentIndex + openTag.length + 1;
242 
243         int endIndex = KMPSearch.search(
244             oldByteArray, beginIndex, _MARKER_SCRIPT_CLOSE,
245             _MARKER_SCRIPT_CLOSE_NEXTS);
246 
247         if (endIndex == -1) {
248             _log.error("Missing </script>");
249 
250             return currentIndex + 1;
251         }
252 
253         int newBeginIndex = endIndex + _MARKER_SCRIPT_CLOSE.length;
254 
255         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
256 
257         String content = new String(
258             oldByteArray, beginIndex, endIndex - beginIndex);
259 
260         if (Validator.isNull(content)) {
261             return newBeginIndex;
262         }
263 
264         content = MinifierUtil.minifyJavaScript(content);
265 
266         if (Validator.isNull(content)) {
267             return newBeginIndex;
268         }
269 
270         newBytes.write(_SCRIPT_TYPE_JAVASCRIPT);
271         newBytes.write(_CDATA_OPEN);
272         newBytes.write(content.getBytes());
273         newBytes.write(_CDATA_CLOSE);
274         newBytes.write(_MARKER_SCRIPT_CLOSE);
275 
276         return newBeginIndex;
277     }
278 
279     protected int processPre(
280         byte[] oldByteArray, UnsyncByteArrayOutputStream newBytes,
281         int currentIndex) {
282 
283         int beginIndex = currentIndex + _MARKER_PRE_OPEN.length + 1;
284 
285         int endIndex = KMPSearch.search(
286             oldByteArray, beginIndex, _MARKER_PRE_CLOSE,
287             _MARKER_PRE_CLOSE_NEXTS);
288 
289         if (endIndex == -1) {
290             _log.error("Missing </pre>");
291 
292             return currentIndex + 1;
293         }
294 
295         int newBeginIndex = endIndex + _MARKER_PRE_CLOSE.length;
296 
297         newBytes.write(
298             oldByteArray, currentIndex, newBeginIndex - currentIndex);
299 
300         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
301 
302         return newBeginIndex;
303     }
304 
305     protected int processTextArea(
306         byte[] oldByteArray, UnsyncByteArrayOutputStream newBytes,
307         int currentIndex) {
308 
309         int beginIndex = currentIndex + _MARKER_TEXTAREA_OPEN.length + 1;
310 
311         int endIndex = KMPSearch.search(
312             oldByteArray, beginIndex, _MARKER_TEXTAREA_CLOSE,
313             _MARKER_TEXTAREA_CLOSE_NEXTS);
314 
315         if (endIndex == -1) {
316             _log.error("Missing </textArea>");
317 
318             return currentIndex + 1;
319         }
320 
321         int newBeginIndex = endIndex + _MARKER_TEXTAREA_CLOSE.length;
322 
323         newBytes.write(
324             oldByteArray, currentIndex, newBeginIndex - currentIndex);
325 
326         newBeginIndex += countContinuousWhiteSpace(oldByteArray, newBeginIndex);
327 
328         return newBeginIndex;
329     }
330 
331     protected byte[] strip(byte[] oldByteArray) {
332         UnsyncByteArrayOutputStream newBytes = new UnsyncByteArrayOutputStream(
333             (int)(oldByteArray.length * _COMPRESSION_RATE));
334 
335         int count = countContinuousWhiteSpace(oldByteArray, 0);
336 
337         for (int i = count; i < oldByteArray.length; i++) {
338             byte b = oldByteArray[i];
339 
340             if (b == CharPool.LESS_THAN) {
341                 if (hasMarker(oldByteArray, i, _MARKER_PRE_OPEN)) {
342                     i = processPre(oldByteArray, newBytes, i) - 1;
343 
344                     continue;
345                 }
346                 else if (hasMarker(oldByteArray, i, _MARKER_TEXTAREA_OPEN)) {
347                     i = processTextArea(oldByteArray, newBytes, i) - 1;
348 
349                     continue;
350                 }
351                 else if (hasMarker(oldByteArray, i, _MARKER_JS_OPEN)) {
352                     i = processJavaScript(
353                             oldByteArray, newBytes, i, _MARKER_JS_OPEN) - 1;
354 
355                     continue;
356                 }
357                 else if (hasMarker(oldByteArray, i, _MARKER_SCRIPT_OPEN)) {
358                     i = processJavaScript(
359                             oldByteArray, newBytes, i, _MARKER_SCRIPT_OPEN) - 1;
360 
361                     continue;
362                 }
363                 else if (hasMarker(oldByteArray, i, _MARKER_STYLE_OPEN)) {
364                     i = processCSS(oldByteArray, newBytes, i) - 1;
365 
366                     continue;
367                 }
368             }
369             else if (b == CharPool.GREATER_THAN) {
370                 newBytes.write(b);
371 
372                 int spaceCount = countContinuousWhiteSpace(oldByteArray, i + 1);
373 
374                 if (spaceCount > 0) {
375                     i = i + spaceCount;
376 
377                     newBytes.write(CharPool.SPACE);
378                 }
379 
380                 continue;
381             }
382 
383             int spaceCount = countContinuousWhiteSpace(oldByteArray, i);
384 
385             if (spaceCount > 0) {
386                 newBytes.write(CharPool.SPACE);
387 
388                 i = i + spaceCount - 1;
389             }
390             else {
391                 newBytes.write(b);
392             }
393         }
394 
395         return newBytes.toByteArray();
396     }
397 
398     private static final byte[] _CDATA_CLOSE = "/*]]>*/".getBytes();
399 
400     private static final byte[] _CDATA_OPEN = "/*<![CDATA[*/".getBytes();
401 
402     private static final double _COMPRESSION_RATE = 0.7;
403 
404     private static final byte[] _MARKER_JS_OPEN =
405         "script type=\"text/javascript\">".getBytes();
406 
407     private static final byte[] _MARKER_PRE_CLOSE = "/pre>".getBytes();
408 
409     private static final int[] _MARKER_PRE_CLOSE_NEXTS =
410         KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
411 
412     private static final byte[] _MARKER_PRE_OPEN = "pre>".getBytes();
413 
414     private static final byte[] _MARKER_SCRIPT_CLOSE = "</script>".getBytes();
415 
416     private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
417         KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
418 
419     private static final byte[] _MARKER_SCRIPT_OPEN = "script>".getBytes();
420 
421     private static final byte[] _MARKER_STYLE_CLOSE = "</style>".getBytes();
422 
423     private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
424         KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
425 
426     private static final byte[] _MARKER_STYLE_OPEN =
427         "style type=\"text/css\">".getBytes();
428 
429     private static final byte[] _MARKER_TEXTAREA_CLOSE =
430         "/textarea>".getBytes();
431 
432     private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
433         KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
434 
435     private static final byte[] _MARKER_TEXTAREA_OPEN =
436         "textarea ".getBytes();
437 
438     private static final byte[] _SCRIPT_TYPE_JAVASCRIPT =
439         "<script type=\"text/javascript\">".getBytes();
440 
441     private static final String _STRIP = "strip";
442 
443     private static final byte[] _STYLE_TYPE_CSS =
444         "<style type=\"text/css\">".getBytes();
445 
446     private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
447 
448 }