001
014
015 package com.liferay.portal.servlet.filters.strip;
016
017 import com.liferay.portal.kernel.cache.key.CacheKeyGenerator;
018 import com.liferay.portal.kernel.cache.key.CacheKeyGeneratorUtil;
019 import com.liferay.portal.kernel.concurrent.ConcurrentLFUCache;
020 import com.liferay.portal.kernel.io.OutputStreamWriter;
021 import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
022 import com.liferay.portal.kernel.log.Log;
023 import com.liferay.portal.kernel.log.LogFactoryUtil;
024 import com.liferay.portal.kernel.portlet.LiferayWindowState;
025 import com.liferay.portal.kernel.scripting.ScriptingException;
026 import com.liferay.portal.kernel.servlet.BufferCacheServletResponse;
027 import com.liferay.portal.kernel.servlet.HttpHeaders;
028 import com.liferay.portal.kernel.servlet.ServletResponseUtil;
029 import com.liferay.portal.kernel.util.CharPool;
030 import com.liferay.portal.kernel.util.GetterUtil;
031 import com.liferay.portal.kernel.util.HttpUtil;
032 import com.liferay.portal.kernel.util.JavaConstants;
033 import com.liferay.portal.kernel.util.KMPSearch;
034 import com.liferay.portal.kernel.util.ParamUtil;
035 import com.liferay.portal.kernel.util.StringPool;
036 import com.liferay.portal.kernel.util.Validator;
037 import com.liferay.portal.servlet.filters.BasePortalFilter;
038 import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
039 import com.liferay.portal.util.MinifierUtil;
040 import com.liferay.portal.util.PropsValues;
041
042 import java.io.Writer;
043
044 import java.nio.CharBuffer;
045
046 import java.util.HashSet;
047 import java.util.Set;
048 import java.util.regex.Matcher;
049 import java.util.regex.Pattern;
050
051 import javax.servlet.FilterChain;
052 import javax.servlet.FilterConfig;
053 import javax.servlet.ServletContext;
054 import javax.servlet.http.HttpServletRequest;
055 import javax.servlet.http.HttpServletResponse;
056
057
062 public class StripFilter extends BasePortalFilter {
063
064 public static final String SKIP_FILTER =
065 StripFilter.class.getName() + "SKIP_FILTER";
066
067 public StripFilter() {
068 if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
069 _minifierCache = new ConcurrentLFUCache<String, String>(
070 PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE);
071 }
072 }
073
074 @Override
075 public void init(FilterConfig filterConfig) {
076 super.init(filterConfig);
077
078 for (String ignorePath : PropsValues.STRIP_IGNORE_PATHS) {
079 _ignorePaths.add(ignorePath);
080 }
081
082 _servletContext = filterConfig.getServletContext();
083 }
084
085 @Override
086 public boolean isFilterEnabled(
087 HttpServletRequest request, HttpServletResponse response) {
088
089 if (isStrip(request) && !isInclude(request) &&
090 !isAlreadyFiltered(request)) {
091
092 return true;
093 }
094 else {
095 return false;
096 }
097 }
098
099 protected String extractContent(CharBuffer charBuffer, int length) {
100
101
102
103
108
109 CharBuffer duplicateCharBuffer = charBuffer.duplicate();
110
111 int position = duplicateCharBuffer.position() + length;
112
113 String content = duplicateCharBuffer.limit(position).toString();
114
115 charBuffer.position(position);
116
117 return content;
118 }
119
120 protected boolean hasLanguageAttribute(
121 CharBuffer charBuffer, int startPos, int length) {
122
123 if (!PropsValues.STRIP_JS_LANGUAGE_ATTRIBUTE_SUPPORT_ENABLED) {
124 return false;
125 }
126
127 if (KMPSearch.search(
128 charBuffer, startPos, length, _MARKER_LANGUAGE,
129 _MARKER_LANGUAGE_NEXTS) == -1) {
130
131 return false;
132 }
133
134 Matcher matcher = _javaScriptPattern.matcher(charBuffer);
135
136 if (matcher.find()) {
137 return true;
138 }
139
140 return false;
141 }
142
143 protected boolean hasMarker(CharBuffer charBuffer, char[] marker) {
144 int position = charBuffer.position();
145
146 if ((position + marker.length) >= charBuffer.limit()) {
147 return false;
148 }
149
150 for (int i = 0; i < marker.length; i++) {
151 char c = marker[i];
152
153 char oldC = charBuffer.charAt(i);
154
155 if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
156 return false;
157 }
158 }
159
160 return true;
161 }
162
163 protected boolean isAlreadyFiltered(HttpServletRequest request) {
164 if (request.getAttribute(SKIP_FILTER) != null) {
165 return true;
166 }
167 else {
168 return false;
169 }
170 }
171
172 protected boolean isInclude(HttpServletRequest request) {
173 String uri = (String)request.getAttribute(
174 JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
175
176 if (uri == null) {
177 return false;
178 }
179 else {
180 return true;
181 }
182 }
183
184 protected boolean isStrip(HttpServletRequest request) {
185 if (!ParamUtil.getBoolean(request, _STRIP, true)) {
186 return false;
187 }
188
189 String path = request.getPathInfo();
190
191 if (_ignorePaths.contains(path)) {
192 if (_log.isDebugEnabled()) {
193 _log.debug("Ignore path " + path);
194 }
195
196 return false;
197 }
198
199
200
201
202
203 String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
204
205 if ((lifecycle.equals("1") &&
206 LiferayWindowState.isExclusive(request)) ||
207 lifecycle.equals("2")) {
208
209 return false;
210 }
211 else {
212 return true;
213 }
214 }
215
216 protected boolean isStripContentType(String contentType) {
217 for (String stripContentType : PropsValues.STRIP_MIME_TYPES) {
218 if (stripContentType.endsWith(StringPool.STAR)) {
219 stripContentType = stripContentType.substring(
220 0, stripContentType.length() - 1);
221
222 if (contentType.startsWith(stripContentType)) {
223 return true;
224 }
225 }
226 else {
227 if (contentType.equals(stripContentType)) {
228 return true;
229 }
230 }
231 }
232
233 return false;
234 }
235
236 protected void outputCloseTag(
237 CharBuffer charBuffer, Writer writer, String closeTag)
238 throws Exception {
239
240 writer.write(closeTag);
241
242 charBuffer.position(charBuffer.position() + closeTag.length());
243
244 skipWhiteSpace(charBuffer, writer, true);
245 }
246
247 protected void outputOpenTag(
248 CharBuffer charBuffer, Writer writer, char[] openTag)
249 throws Exception {
250
251 writer.write(openTag);
252
253 charBuffer.position(charBuffer.position() + openTag.length);
254 }
255
256 protected void processCSS(
257 HttpServletRequest request, HttpServletResponse response,
258 CharBuffer charBuffer, Writer writer)
259 throws Exception {
260
261 outputOpenTag(charBuffer, writer, _MARKER_STYLE_OPEN);
262
263 int length = KMPSearch.search(
264 charBuffer, _MARKER_STYLE_CLOSE, _MARKER_STYLE_CLOSE_NEXTS);
265
266 if (length == -1) {
267 if (_log.isWarnEnabled()) {
268 _log.warn("Missing </style>");
269 }
270
271 return;
272 }
273
274 if (length == 0) {
275 outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
276
277 return;
278 }
279
280 String content = extractContent(charBuffer, length);
281
282 String minifiedContent = content;
283
284 if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
285 CacheKeyGenerator cacheKeyGenerator =
286 CacheKeyGeneratorUtil.getCacheKeyGenerator(
287 StripFilter.class.getName());
288
289 String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
290
291 minifiedContent = _minifierCache.get(key);
292
293 if (minifiedContent == null) {
294 if (PropsValues.STRIP_CSS_SASS_ENABLED) {
295 try {
296 content = DynamicCSSUtil.parseSass(
297 _servletContext, request, null, content);
298 }
299 catch (ScriptingException se) {
300 _log.error("Unable to parse SASS on CSS " + key, se);
301
302 if (_log.isDebugEnabled()) {
303 _log.debug(content);
304 }
305
306 if (response != null) {
307 response.setHeader(
308 HttpHeaders.CACHE_CONTROL,
309 HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
310 }
311 }
312 }
313
314 minifiedContent = MinifierUtil.minifyCss(content);
315
316 boolean skipCache = false;
317
318 for (String skipCss :
319 PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SKIP_CSS) {
320
321 if (minifiedContent.contains(skipCss)) {
322 skipCache = true;
323
324 break;
325 }
326 }
327
328 if (!skipCache) {
329 _minifierCache.put(key, minifiedContent);
330 }
331 }
332 }
333
334 if (!Validator.isNull(minifiedContent)) {
335 writer.write(minifiedContent);
336 }
337
338 outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
339 }
340
341 @Override
342 protected void processFilter(
343 HttpServletRequest request, HttpServletResponse response,
344 FilterChain filterChain)
345 throws Exception {
346
347 if (_log.isDebugEnabled()) {
348 String completeURL = HttpUtil.getCompleteURL(request);
349
350 _log.debug("Stripping " + completeURL);
351 }
352
353 request.setAttribute(SKIP_FILTER, Boolean.TRUE);
354
355 BufferCacheServletResponse bufferCacheServletResponse =
356 new BufferCacheServletResponse(response);
357
358 processFilter(
359 StripFilter.class, request, bufferCacheServletResponse,
360 filterChain);
361
362 String contentType = GetterUtil.getString(
363 bufferCacheServletResponse.getContentType()).toLowerCase();
364
365 if (_log.isDebugEnabled()) {
366 _log.debug("Stripping content of type " + contentType);
367 }
368
369 response.setContentType(contentType);
370
371 if (isStripContentType(contentType) &&
372 (bufferCacheServletResponse.getStatus() ==
373 HttpServletResponse.SC_OK)) {
374
375 CharBuffer oldCharBuffer =
376 bufferCacheServletResponse.getCharBuffer();
377
378 boolean ensureContentLength = ParamUtil.getBoolean(
379 request, _ENSURE_CONTENT_LENGTH);
380
381 if (ensureContentLength) {
382 UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
383 new UnsyncByteArrayOutputStream();
384
385 strip(
386 request, response, oldCharBuffer,
387 new OutputStreamWriter(unsyncByteArrayOutputStream));
388
389 response.setContentLength(unsyncByteArrayOutputStream.size());
390
391 unsyncByteArrayOutputStream.writeTo(response.getOutputStream());
392 }
393 else if (!response.isCommitted()) {
394 strip(request, response, oldCharBuffer, response.getWriter());
395 }
396 }
397 else {
398 ServletResponseUtil.write(response, bufferCacheServletResponse);
399 }
400 }
401
402 protected void processInput(CharBuffer oldCharBuffer, Writer writer)
403 throws Exception {
404
405 int length = KMPSearch.search(
406 oldCharBuffer, _MARKER_INPUT_OPEN.length + 1, _MARKER_INPUT_CLOSE,
407 _MARKER_INPUT_CLOSE_NEXTS);
408
409 if (length == -1) {
410 if (_log.isWarnEnabled()) {
411 _log.warn("Missing />");
412 }
413
414 outputOpenTag(oldCharBuffer, writer, _MARKER_INPUT_OPEN);
415
416 return;
417 }
418
419 length += _MARKER_INPUT_CLOSE.length();
420
421 String content = extractContent(oldCharBuffer, length);
422
423 writer.write(content);
424
425 skipWhiteSpace(oldCharBuffer, writer, true);
426 }
427
428 protected void processJavaScript(
429 CharBuffer charBuffer, Writer writer, char[] openTag)
430 throws Exception {
431
432 int endPos = openTag.length + 1;
433
434 char c = charBuffer.charAt(openTag.length);
435
436 if (c == CharPool.SPACE) {
437 int startPos = openTag.length + 1;
438
439 for (int i = startPos; i < charBuffer.length(); i++) {
440 c = charBuffer.charAt(i);
441
442 if (c == CharPool.GREATER_THAN) {
443
444
445
446 endPos = i + 1;
447
448 int length = i - startPos;
449
450 if ((length < _MARKER_TYPE_JAVASCRIPT.length()) ||
451 (KMPSearch.search(
452 charBuffer, startPos, length,
453 _MARKER_TYPE_JAVASCRIPT,
454 _MARKER_TYPE_JAVASCRIPT_NEXTS) == -1)) {
455
456
457
458
459
460
461
462 if (!hasLanguageAttribute(
463 charBuffer, startPos, length)) {
464
465 return;
466 }
467 }
468
469
470
471
472 break;
473 }
474 else if (c == CharPool.LESS_THAN) {
475
476
477
478 return;
479 }
480 }
481
482 if (endPos == charBuffer.length()) {
483
484
485
486 return;
487 }
488 }
489 else if (c != CharPool.GREATER_THAN) {
490
491
492
493 return;
494 }
495
496 writer.append(charBuffer, 0, endPos);
497
498 charBuffer.position(charBuffer.position() + endPos);
499
500 int length = KMPSearch.search(
501 charBuffer, _MARKER_SCRIPT_CLOSE, _MARKER_SCRIPT_CLOSE_NEXTS);
502
503 if (length == -1) {
504 if (_log.isWarnEnabled()) {
505 _log.warn("Missing </script>");
506 }
507
508 return;
509 }
510
511 if (length == 0) {
512 outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
513
514 return;
515 }
516
517 String content = extractContent(charBuffer, length);
518
519 String minifiedContent = content;
520
521 if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
522 CacheKeyGenerator cacheKeyGenerator =
523 CacheKeyGeneratorUtil.getCacheKeyGenerator(
524 StripFilter.class.getName());
525
526 String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
527
528 minifiedContent = _minifierCache.get(key);
529
530 if (minifiedContent == null) {
531 minifiedContent = MinifierUtil.minifyJavaScript(content);
532
533 boolean skipCache = false;
534
535 for (String skipJavaScript :
536 PropsValues.
537 MINIFIER_INLINE_CONTENT_CACHE_SKIP_JAVASCRIPT) {
538
539 if (minifiedContent.contains(skipJavaScript)) {
540 skipCache = true;
541
542 break;
543 }
544 }
545
546 if (!skipCache) {
547 _minifierCache.put(key, minifiedContent);
548 }
549 }
550 }
551
552 if (!Validator.isNull(minifiedContent)) {
553 writer.write(minifiedContent);
554 }
555
556 outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
557 }
558
559 protected void processPre(CharBuffer oldCharBuffer, Writer writer)
560 throws Exception {
561
562 int length = KMPSearch.search(
563 oldCharBuffer, _MARKER_PRE_OPEN.length + 1, _MARKER_PRE_CLOSE,
564 _MARKER_PRE_CLOSE_NEXTS);
565
566 if (length == -1) {
567 if (_log.isWarnEnabled()) {
568 _log.warn("Missing </pre>");
569 }
570
571 outputOpenTag(oldCharBuffer, writer, _MARKER_PRE_OPEN);
572
573 return;
574 }
575
576 length += _MARKER_PRE_CLOSE.length();
577
578 String content = extractContent(oldCharBuffer, length);
579
580 writer.write(content);
581
582 skipWhiteSpace(oldCharBuffer, writer, true);
583 }
584
585 protected void processTextArea(CharBuffer oldCharBuffer, Writer writer)
586 throws Exception {
587
588 int length = KMPSearch.search(
589 oldCharBuffer, _MARKER_TEXTAREA_OPEN.length + 1,
590 _MARKER_TEXTAREA_CLOSE, _MARKER_TEXTAREA_CLOSE_NEXTS);
591
592 if (length == -1) {
593 if (_log.isWarnEnabled()) {
594 _log.warn("Missing </textArea>");
595 }
596
597 outputOpenTag(oldCharBuffer, writer, _MARKER_TEXTAREA_OPEN);
598 return;
599 }
600
601 length += _MARKER_TEXTAREA_CLOSE.length();
602
603 String content = extractContent(oldCharBuffer, length);
604
605 writer.write(content);
606
607 skipWhiteSpace(oldCharBuffer, writer, true);
608 }
609
610 protected boolean skipWhiteSpace(
611 CharBuffer charBuffer, Writer writer, boolean appendSeparator)
612 throws Exception {
613
614 boolean skipped = false;
615
616 for (int i = charBuffer.position(); i < charBuffer.limit(); i++) {
617 char c = charBuffer.get();
618
619 if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
620 (c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
621
622 skipped = true;
623
624 continue;
625 }
626 else {
627 charBuffer.position(i);
628
629 break;
630 }
631 }
632
633 if (skipped && appendSeparator) {
634 writer.write(CharPool.SPACE);
635 }
636
637 return skipped;
638 }
639
640 protected void strip(
641 HttpServletRequest request, HttpServletResponse response,
642 CharBuffer charBuffer, Writer writer)
643 throws Exception {
644
645 skipWhiteSpace(charBuffer, writer, false);
646
647 while (charBuffer.hasRemaining()) {
648 char c = charBuffer.get();
649
650 writer.write(c);
651
652 if (c == CharPool.LESS_THAN) {
653 if (hasMarker(charBuffer, _MARKER_INPUT_OPEN)) {
654 processInput(charBuffer, writer);
655
656 continue;
657 }
658 else if (hasMarker(charBuffer, _MARKER_PRE_OPEN)) {
659 processPre(charBuffer, writer);
660
661 continue;
662 }
663 else if (hasMarker(charBuffer, _MARKER_TEXTAREA_OPEN)) {
664 processTextArea(charBuffer, writer);
665
666 continue;
667 }
668 else if (hasMarker(charBuffer, _MARKER_SCRIPT_OPEN)) {
669 processJavaScript(charBuffer, writer, _MARKER_SCRIPT_OPEN);
670
671 continue;
672 }
673 else if (hasMarker(charBuffer, _MARKER_STYLE_OPEN)) {
674 processCSS(request, response, charBuffer, writer);
675
676 continue;
677 }
678 }
679 else if (c == CharPool.GREATER_THAN) {
680 skipWhiteSpace(charBuffer, writer, true);
681 }
682
683 skipWhiteSpace(charBuffer, writer, true);
684 }
685
686 writer.flush();
687 }
688
689 private static final String _ENSURE_CONTENT_LENGTH = "ensureContentLength";
690
691 private static final String _MARKER_INPUT_CLOSE = "/>";
692
693 private static final int[] _MARKER_INPUT_CLOSE_NEXTS =
694 KMPSearch.generateNexts(_MARKER_INPUT_CLOSE);
695
696 private static final char[] _MARKER_INPUT_OPEN = "input".toCharArray();
697
698 private static final String _MARKER_LANGUAGE = "language=";
699
700 private static final int[] _MARKER_LANGUAGE_NEXTS = KMPSearch.generateNexts(
701 _MARKER_LANGUAGE);
702
703 private static final String _MARKER_PRE_CLOSE = "/pre>";
704
705 private static final int[] _MARKER_PRE_CLOSE_NEXTS =
706 KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
707
708 private static final char[] _MARKER_PRE_OPEN = "pre".toCharArray();
709
710 private static final String _MARKER_SCRIPT_CLOSE = "</script>";
711
712 private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
713 KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
714
715 private static final char[] _MARKER_SCRIPT_OPEN = "script".toCharArray();
716
717 private static final String _MARKER_STYLE_CLOSE = "</style>";
718
719 private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
720 KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
721
722 private static final char[] _MARKER_STYLE_OPEN =
723 "style type=\"text/css\">".toCharArray();
724
725 private static final String _MARKER_TEXTAREA_CLOSE = "/textarea>";
726
727 private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
728 KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
729
730 private static final char[] _MARKER_TEXTAREA_OPEN =
731 "textarea ".toCharArray();
732
733 private static final String _MARKER_TYPE_JAVASCRIPT =
734 "type=\"text/javascript\"";
735
736 private static final int[] _MARKER_TYPE_JAVASCRIPT_NEXTS =
737 KMPSearch.generateNexts(_MARKER_TYPE_JAVASCRIPT);
738
739 private static final String _STRIP = "strip";
740
741 private static Log _log = LogFactoryUtil.getLog(StripFilter.class);
742
743 private static Pattern _javaScriptPattern = Pattern.compile(
744 "[Jj][aA][vV][aA][sS][cC][rR][iI][pP][tT]");
745
746 private Set<String> _ignorePaths = new HashSet<String>();
747 private ConcurrentLFUCache<String, String> _minifierCache;
748 private ServletContext _servletContext;
749
750 }