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