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