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