1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    * Permission is hereby granted, free of charge, to any person obtaining a copy
5    * of this software and associated documentation files (the "Software"), to deal
6    * in the Software without restriction, including without limitation the rights
7    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    * copies of the Software, and to permit persons to whom the Software is
9    * furnished to do so, subject to the following conditions:
10   *
11   * The above copyright notice and this permission notice shall be included in
12   * all copies or substantial portions 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.log.Log;
26  import com.liferay.portal.kernel.log.LogFactoryUtil;
27  import com.liferay.portal.kernel.portlet.LiferayWindowState;
28  import com.liferay.portal.kernel.util.CharPool;
29  import com.liferay.portal.kernel.util.GetterUtil;
30  import com.liferay.portal.kernel.util.HttpUtil;
31  import com.liferay.portal.kernel.util.JavaConstants;
32  import com.liferay.portal.kernel.util.ParamUtil;
33  import com.liferay.portal.kernel.util.StringPool;
34  import com.liferay.portal.kernel.util.Validator;
35  import com.liferay.portal.servlet.filters.BasePortalFilter;
36  import com.liferay.portal.servlet.filters.etag.ETagUtil;
37  import com.liferay.portal.util.MinifierUtil;
38  import com.liferay.util.servlet.ServletResponseUtil;
39  
40  import java.io.ByteArrayOutputStream;
41  import java.io.IOException;
42  
43  import javax.servlet.FilterChain;
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   *
53   */
54  public class StripFilter extends BasePortalFilter {
55  
56      public static final String SKIP_FILTER =
57          StripFilter.class.getName() + "SKIP_FILTER";
58  
59      protected boolean hasMarker(byte[] oldByteArray, int pos, char[] marker) {
60          if ((pos + marker.length) >= oldByteArray.length) {
61              return false;
62          }
63  
64          for (int i = 0; i < marker.length; i++) {
65              char c = marker[i];
66  
67              char oldC = (char)oldByteArray[pos + i + 1];
68  
69              if ((c != oldC) &&
70                  (Character.toUpperCase(c) != oldC)) {
71  
72                  return false;
73              }
74          }
75  
76          return true;
77      }
78  
79      protected boolean isAlreadyFiltered(HttpServletRequest request) {
80          if (request.getAttribute(SKIP_FILTER) != null) {
81              return true;
82          }
83          else {
84              return false;
85          }
86      }
87  
88      protected boolean isInclude(HttpServletRequest request) {
89          String uri = (String)request.getAttribute(
90              JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
91  
92          if (uri == null) {
93              return false;
94          }
95          else {
96              return true;
97          }
98      }
99  
100     protected boolean isStrip(HttpServletRequest request) {
101         if (!ParamUtil.getBoolean(request, _STRIP, true)) {
102             return false;
103         }
104         else {
105 
106             // Modifying binary content through a servlet filter under certain
107             // conditions is bad on performance the user will not start
108             // downloading the content until the entire content is modified.
109 
110             String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
111 
112             if ((lifecycle.equals("1") &&
113                  LiferayWindowState.isExclusive(request)) ||
114                 lifecycle.equals("2")) {
115 
116                 return false;
117             }
118             else {
119                 return true;
120             }
121         }
122     }
123 
124     protected void processFilter(
125             HttpServletRequest request, HttpServletResponse response,
126             FilterChain filterChain)
127         throws Exception {
128 
129         if (isStrip(request) && !isInclude(request) &&
130             !isAlreadyFiltered(request)) {
131 
132             if (_log.isDebugEnabled()) {
133                 String completeURL = HttpUtil.getCompleteURL(request);
134 
135                 _log.debug("Stripping " + completeURL);
136             }
137 
138             request.setAttribute(SKIP_FILTER, Boolean.TRUE);
139 
140             StripResponse stripResponse = new StripResponse(response);
141 
142             processFilter(
143                 StripFilter.class, request, stripResponse, filterChain);
144 
145             String contentType = GetterUtil.getString(
146                 stripResponse.getContentType()).toLowerCase();
147 
148             byte[] oldByteArray = stripResponse.getData();
149 
150             if ((oldByteArray != null) && (oldByteArray.length > 0)) {
151                 byte[] newByteArray = null;
152                 int newByteArrayPos = 0;
153 
154                 if (_log.isDebugEnabled()) {
155                     _log.debug("Stripping content of type " + contentType);
156                 }
157 
158                 if (contentType.indexOf("text/") != -1) {
159                     Object[] value = strip(oldByteArray);
160 
161                     newByteArray = (byte[])value[0];
162                     newByteArrayPos = (Integer)value[1];
163                 }
164                 else {
165                     newByteArray = oldByteArray;
166                     newByteArrayPos = oldByteArray.length;
167                 }
168 
169                 if (!ETagUtil.processETag(request, response, newByteArray)) {
170                     ServletResponseUtil.write(
171                         response, newByteArray, newByteArrayPos);
172                 }
173             }
174         }
175         else {
176             if (_log.isDebugEnabled()) {
177                 String completeURL = HttpUtil.getCompleteURL(request);
178 
179                 _log.debug("Not stripping " + completeURL);
180             }
181 
182             processFilter(StripFilter.class, request, response, filterChain);
183         }
184     }
185 
186     protected Object[] strip(byte[] oldByteArray) throws IOException {
187         ByteArrayOutputStream newBytes = new ByteArrayOutputStream(
188             oldByteArray.length);
189 
190         int state = _STATE_NORMAL;
191 
192         boolean removeStartingWhitespace = true;
193 
194         ByteArrayOutputStream scriptBytes = new ByteArrayOutputStream();
195         ByteArrayOutputStream styleBytes = new ByteArrayOutputStream();
196 
197         for (int i = 0; i < oldByteArray.length; i++) {
198             byte b = oldByteArray[i];
199 
200             char c = (char)b;
201 
202             if (c == CharPool.LESS_THAN) {
203                 if (state == _STATE_NORMAL) {
204                     if (hasMarker(oldByteArray, i, _MARKER_PRE_OPEN) ||
205                         hasMarker(oldByteArray, i, _MARKER_TEXTAREA_OPEN)) {
206 
207                         state = _STATE_IGNORE;
208                     }
209                     else if (hasMarker(oldByteArray, i, _MARKER_DIV_CLOSE) ||
210                              hasMarker(oldByteArray, i, _MARKER_FORM_CLOSE) ||
211                              hasMarker(oldByteArray, i, _MARKER_LI_CLOSE) ||
212                              hasMarker(oldByteArray, i, _MARKER_SCRIPT_CLOSE) ||
213                              hasMarker(oldByteArray, i, _MARKER_STYLE_CLOSE) ||
214                              hasMarker(oldByteArray, i, _MARKER_TABLE_CLOSE) ||
215                              hasMarker(oldByteArray, i, _MARKER_TD_CLOSE) ||
216                              hasMarker(oldByteArray, i, _MARKER_TD_OPEN) ||
217                              hasMarker(oldByteArray, i, _MARKER_TR_CLOSE) ||
218                              hasMarker(oldByteArray, i, _MARKER_TR_OPEN) ||
219                              hasMarker(oldByteArray, i, _MARKER_UL_CLOSE)) {
220 
221                         state = _STATE_FOUND_ELEMENT;
222                     }
223                     else if (hasMarker(
224                                 oldByteArray, i, _MARKER_JAVASCRIPT_OPEN) ||
225                              hasMarker(oldByteArray, i, _MARKER_SCRIPT_OPEN)) {
226 
227                         state = _STATE_MINIFY_SCRIPT;
228                     }
229                     else if (hasMarker(oldByteArray, i, _MARKER_STYLE_OPEN)) {
230                         state = _STATE_MINIFY_STYLE;
231                     }
232                 }
233                 else if (state == _STATE_IGNORE) {
234                     if (hasMarker(oldByteArray, i, _MARKER_PRE_CLOSE) ||
235                         hasMarker(oldByteArray, i, _MARKER_TEXTAREA_CLOSE)) {
236 
237                         state = _STATE_NORMAL;
238                     }
239                 }
240                 else if (state == _STATE_MINIFY_SCRIPT) {
241                     if (hasMarker(oldByteArray, i, _MARKER_SCRIPT_CLOSE)) {
242                         state = _STATE_NORMAL;
243 
244                         String scriptContent = scriptBytes.toString(
245                             StringPool.UTF8);
246 
247                         scriptBytes = new ByteArrayOutputStream();
248 
249                         int pos = scriptContent.indexOf(CharPool.GREATER_THAN);
250 
251                         scriptContent = scriptContent.substring(pos + 1).trim();
252 
253                         if (Validator.isNull(scriptContent)) {
254                             i += _MARKER_SCRIPT_CLOSE.length;
255 
256                             continue;
257                         }
258 
259                         scriptContent = MinifierUtil.minifyJavaScript(
260                             scriptContent);
261 
262                         if (Validator.isNull(scriptContent)) {
263                             i += _MARKER_SCRIPT_CLOSE.length;
264 
265                             continue;
266                         }
267 
268                         scriptContent =
269                             _SCRIPT_TYPE_JAVASCRIPT + _CDATA_OPEN +
270                                 scriptContent + _CDATA_CLOSE;
271 
272                         byte[] scriptContentBytes = scriptContent.getBytes(
273                             StringPool.UTF8);
274 
275                         for (byte curByte : scriptContentBytes) {
276                             newBytes.write(curByte);
277                         }
278 
279                         state = _STATE_FOUND_ELEMENT;
280                     }
281                 }
282                 else if (state == _STATE_MINIFY_STYLE) {
283                     if (hasMarker(oldByteArray, i, _MARKER_STYLE_CLOSE)) {
284                         state = _STATE_NORMAL;
285 
286                         String styleContent = styleBytes.toString(
287                             StringPool.UTF8);
288 
289                         styleBytes = new ByteArrayOutputStream();
290 
291                         styleContent = styleContent.substring(
292                             _STYLE_TYPE_CSS.length()).trim();
293 
294                         if (Validator.isNull(styleContent)) {
295                             i += _MARKER_STYLE_CLOSE.length;
296 
297                             continue;
298                         }
299 
300                         styleContent = MinifierUtil.minifyCss(styleContent);
301 
302                         if (Validator.isNull(styleContent)) {
303                             i += _MARKER_STYLE_CLOSE.length;
304 
305                             continue;
306                         }
307 
308                         styleContent = _STYLE_TYPE_CSS + styleContent;
309 
310                         byte[] styleContentBytes = styleContent.getBytes(
311                             StringPool.UTF8);
312 
313                         for (byte curByte : styleContentBytes) {
314                             newBytes.write(curByte);
315                         }
316 
317                         state = _STATE_FOUND_ELEMENT;
318                     }
319                 }
320             }
321             else if (c == CharPool.GREATER_THAN) {
322                 if (state == _STATE_FOUND_ELEMENT) {
323                     state = _STATE_NORMAL;
324 
325                     newBytes.write(b);
326 
327                     while ((i + 1) < oldByteArray.length) {
328                         char nextChar = (char)oldByteArray[i + 1];
329 
330                         if (Validator.isWhitespace(nextChar)) {
331                             i++;
332                         }
333                         else {
334                             break;
335                         }
336                     }
337 
338                     continue;
339                 }
340             }
341 
342             if (state == _STATE_NORMAL) {
343                 if ((i + 1) < oldByteArray.length) {
344                     if (removeStartingWhitespace) {
345                         if (Validator.isWhitespace(c)) {
346                             continue;
347                         }
348                         else {
349                             removeStartingWhitespace = false;
350                         }
351                     }
352 
353                     if ((c == CharPool.NEW_LINE) ||
354                         (c == CharPool.RETURN) ||
355                         (c == CharPool.TAB)) {
356 
357                         char nextChar = (char)oldByteArray[i + 1];
358 
359                         if ((nextChar == CharPool.NEW_LINE) ||
360                             (nextChar == CharPool.RETURN) ||
361                             (nextChar == CharPool.TAB)) {
362 
363                             continue;
364                         }
365                     }
366                 }
367             }
368 
369             if (state == _STATE_MINIFY_SCRIPT) {
370                 scriptBytes.write(b);
371             }
372             else if (state == _STATE_MINIFY_STYLE) {
373                 styleBytes.write(b);
374             }
375             else {
376                 newBytes.write(b);
377             }
378         }
379 
380         byte[] newByteArray = newBytes.toByteArray();
381         int newByteArrayPos = newBytes.size();
382 
383         if (newByteArrayPos > 1) {
384             for (int i = newByteArrayPos - 1; i > 0; i--) {
385                 byte b = newByteArray[i];
386 
387                 char c = (char)b;
388 
389                 if (Validator.isWhitespace(c)) {
390                     newByteArrayPos--;
391                 }
392                 else {
393                     break;
394                 }
395             }
396         }
397 
398         if (state == _STATE_MINIFY_SCRIPT) {
399             _log.error("Missing </script>");
400         }
401         else if (state == _STATE_MINIFY_STYLE) {
402             _log.error("Missing </style>");
403         }
404 
405         return new Object[] {newByteArray, newByteArrayPos};
406     }
407 
408     private static final String _CDATA_CLOSE = "/*]]>*/";
409 
410     private static final String _CDATA_OPEN = "/*<![CDATA[*/";
411 
412     private static final char[] _MARKER_DIV_CLOSE = "/div>".toCharArray();
413 
414     private static final char[] _MARKER_FORM_CLOSE = "/form>".toCharArray();
415 
416     private static final char[] _MARKER_JAVASCRIPT_OPEN =
417         "script type=\"text/javascript\">".toCharArray();
418 
419     private static final char[] _MARKER_LI_CLOSE = "/li>".toCharArray();
420 
421     private static final char[] _MARKER_PRE_CLOSE = "/pre>".toCharArray();
422 
423     private static final char[] _MARKER_PRE_OPEN = "pre>".toCharArray();
424 
425     private static final char[] _MARKER_SCRIPT_OPEN = "script>".toCharArray();
426 
427     private static final char[] _MARKER_SCRIPT_CLOSE = "/script>".toCharArray();
428 
429     private static final char[] _MARKER_STYLE_OPEN =
430         "style type=\"text/css\">".toCharArray();
431 
432     private static final char[] _MARKER_STYLE_CLOSE = "/style>".toCharArray();
433 
434     private static final char[] _MARKER_TABLE_CLOSE = "/table>".toCharArray();
435 
436     private static final char[] _MARKER_TD_CLOSE = "/td>".toCharArray();
437 
438     private static final char[] _MARKER_TD_OPEN = "td>".toCharArray();
439 
440     private static final char[] _MARKER_TR_CLOSE = "/tr>".toCharArray();
441 
442     private static final char[] _MARKER_TR_OPEN = "tr>".toCharArray();
443 
444     private static final char[] _MARKER_TEXTAREA_CLOSE =
445         "/textarea>".toCharArray();
446 
447     private static final char[] _MARKER_TEXTAREA_OPEN =
448         "textarea ".toCharArray();
449 
450     private static final char[] _MARKER_UL_CLOSE = "/ul>".toCharArray();
451 
452     private static final String _SCRIPT_TYPE_JAVASCRIPT =
453         "<script type=\"text/javascript\">";
454 
455     private static final int _STATE_FOUND_ELEMENT = 3;
456 
457     private static final int _STATE_IGNORE = 1;
458 
459     private static final int _STATE_MINIFY_SCRIPT = 4;
460 
461     private static final int _STATE_MINIFY_STYLE = 5;
462 
463     private static final int _STATE_NORMAL = 0;
464 
465     private static final String _STYLE_TYPE_CSS = "<style type=\"text/css\">";
466 
467     private static final String _STRIP = "strip";
468 
469     private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
470 
471 }