/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.portal.servlet.filters.strip;
import com.liferay.portal.kernel.cache.key.CacheKeyGenerator;
import com.liferay.portal.kernel.cache.key.CacheKeyGeneratorUtil;
import com.liferay.portal.kernel.concurrent.ConcurrentLFUCache;
import com.liferay.portal.kernel.io.OutputStreamWriter;
import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.portlet.LiferayWindowState;
import com.liferay.portal.kernel.servlet.BufferCacheServletResponse;
import com.liferay.portal.kernel.servlet.ServletResponseUtil;
import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HttpUtil;
import com.liferay.portal.kernel.util.JavaConstants;
import com.liferay.portal.kernel.util.KMPSearch;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.minifier.MinifierUtil;
import com.liferay.portal.servlet.filters.BasePortalFilter;
import com.liferay.portal.util.PropsValues;
import java.io.Writer;
import java.nio.CharBuffer;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Brian Wing Shun Chan
* @author Raymond Augé
* @author Shuyang Zhou
*/
public class StripFilter extends BasePortalFilter {
public static final String SKIP_FILTER =
StripFilter.class.getName() + "#SKIP_FILTER";
public StripFilter() {
if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
_minifierCache = new ConcurrentLFUCache<>(
PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE);
}
else {
_minifierCache = null;
}
}
@Override
public void init(FilterConfig filterConfig) {
super.init(filterConfig);
for (String ignorePath : PropsValues.STRIP_IGNORE_PATHS) {
_ignorePaths.add(ignorePath);
}
}
@Override
public boolean isFilterEnabled(
HttpServletRequest request, HttpServletResponse response) {
if (isStrip(request) && !isInclude(request) &&
!isAlreadyFiltered(request)) {
return true;
}
else {
return false;
}
}
protected String extractContent(CharBuffer charBuffer, int length) {
// See LPS-10545
/*String content = charBuffer.subSequence(0, length).toString();
int position = charBuffer.position();
charBuffer.position(position + length);*/
CharBuffer duplicateCharBuffer = charBuffer.duplicate();
int position = duplicateCharBuffer.position() + length;
String content = duplicateCharBuffer.limit(position).toString();
charBuffer.position(position);
return content;
}
protected boolean hasLanguageAttribute(
CharBuffer charBuffer, int startPos, int length) {
if (!PropsValues.STRIP_JS_LANGUAGE_ATTRIBUTE_SUPPORT_ENABLED) {
return false;
}
if (KMPSearch.search(
charBuffer, startPos, length, _MARKER_LANGUAGE,
_MARKER_LANGUAGE_NEXTS) == -1) {
return false;
}
Matcher matcher = _javaScriptPattern.matcher(charBuffer);
if (matcher.find()) {
return true;
}
return false;
}
protected boolean hasMarker(CharBuffer charBuffer, char[] marker) {
int position = charBuffer.position();
if ((position + marker.length) >= charBuffer.limit()) {
return false;
}
for (int i = 0; i < marker.length; i++) {
char c = marker[i];
char oldC = charBuffer.charAt(i);
if ((c != oldC) && (Character.toUpperCase(c) != oldC)) {
return false;
}
}
return true;
}
protected boolean isAlreadyFiltered(HttpServletRequest request) {
if (request.getAttribute(SKIP_FILTER) != null) {
return true;
}
else {
return false;
}
}
protected boolean isInclude(HttpServletRequest request) {
String uri = (String)request.getAttribute(
JavaConstants.JAVAX_SERVLET_INCLUDE_REQUEST_URI);
if (uri == null) {
return false;
}
else {
return true;
}
}
protected boolean isStrip(HttpServletRequest request) {
if (!ParamUtil.getBoolean(request, _STRIP, true)) {
return false;
}
String path = request.getPathInfo();
if (_ignorePaths.contains(path)) {
if (_log.isDebugEnabled()) {
_log.debug("Ignore path " + path);
}
return false;
}
// Modifying binary content through a servlet filter under certain
// conditions is bad on performance the user will not start downloading
// the content until the entire content is modified.
String lifecycle = ParamUtil.getString(request, "p_p_lifecycle");
if ((lifecycle.equals("1") &&
LiferayWindowState.isExclusive(request)) ||
lifecycle.equals("2")) {
return false;
}
else {
return true;
}
}
protected boolean isStripContentType(String contentType) {
for (String stripContentType : PropsValues.STRIP_MIME_TYPES) {
if (stripContentType.endsWith(StringPool.STAR)) {
stripContentType = stripContentType.substring(
0, stripContentType.length() - 1);
if (contentType.startsWith(stripContentType)) {
return true;
}
}
else {
if (contentType.equals(stripContentType)) {
return true;
}
}
}
return false;
}
protected void outputCloseTag(
CharBuffer charBuffer, Writer writer, String closeTag)
throws Exception {
writer.write(closeTag);
charBuffer.position(charBuffer.position() + closeTag.length());
skipWhiteSpace(charBuffer, writer, true);
}
protected void outputOpenTag(
CharBuffer charBuffer, Writer writer, char[] openTag)
throws Exception {
writer.write(openTag);
charBuffer.position(charBuffer.position() + openTag.length);
}
protected void processCSS(
HttpServletRequest request, HttpServletResponse response,
CharBuffer charBuffer, Writer writer)
throws Exception {
outputOpenTag(charBuffer, writer, _MARKER_STYLE_OPEN);
int length = KMPSearch.search(
charBuffer, _MARKER_STYLE_CLOSE, _MARKER_STYLE_CLOSE_NEXTS);
if (length == -1) {
if (_log.isWarnEnabled()) {
_log.warn("Missing </style>");
}
return;
}
if (length == 0) {
outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
return;
}
String content = extractContent(charBuffer, length);
String minifiedContent = content;
if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
CacheKeyGenerator cacheKeyGenerator =
CacheKeyGeneratorUtil.getCacheKeyGenerator(
StripFilter.class.getName());
String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
minifiedContent = _minifierCache.get(key);
if (minifiedContent == null) {
minifiedContent = MinifierUtil.minifyCss(content);
boolean skipCache = false;
for (String skipCss :
PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SKIP_CSS) {
if (minifiedContent.contains(skipCss)) {
skipCache = true;
break;
}
}
if (!skipCache) {
_minifierCache.put(key, minifiedContent);
}
}
}
if (Validator.isNotNull(minifiedContent)) {
writer.write(minifiedContent);
}
outputCloseTag(charBuffer, writer, _MARKER_STYLE_CLOSE);
}
@Override
protected void processFilter(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws Exception {
if (_log.isDebugEnabled()) {
String completeURL = HttpUtil.getCompleteURL(request);
_log.debug("Stripping " + completeURL);
}
request.setAttribute(SKIP_FILTER, Boolean.TRUE);
BufferCacheServletResponse bufferCacheServletResponse =
new BufferCacheServletResponse(response);
processFilter(
StripFilter.class.getName(), request, bufferCacheServletResponse,
filterChain);
String contentType = GetterUtil.getString(
bufferCacheServletResponse.getContentType());
contentType = StringUtil.toLowerCase(contentType);
if (_log.isDebugEnabled()) {
_log.debug("Stripping content of type " + contentType);
}
response.setContentType(contentType);
if (isStripContentType(contentType) &&
(bufferCacheServletResponse.getStatus() ==
HttpServletResponse.SC_OK)) {
CharBuffer oldCharBuffer =
bufferCacheServletResponse.getCharBuffer();
boolean ensureContentLength = ParamUtil.getBoolean(
request, _ENSURE_CONTENT_LENGTH);
if (ensureContentLength) {
UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
new UnsyncByteArrayOutputStream();
strip(
request, response, oldCharBuffer,
new OutputStreamWriter(unsyncByteArrayOutputStream));
response.setContentLength(unsyncByteArrayOutputStream.size());
unsyncByteArrayOutputStream.writeTo(response.getOutputStream());
}
else if (!response.isCommitted()) {
strip(request, response, oldCharBuffer, response.getWriter());
}
}
else {
ServletResponseUtil.write(response, bufferCacheServletResponse);
}
}
protected void processInput(CharBuffer oldCharBuffer, Writer writer)
throws Exception {
int length = KMPSearch.search(
oldCharBuffer, _MARKER_INPUT_OPEN.length + 1, _MARKER_INPUT_CLOSE,
_MARKER_INPUT_CLOSE_NEXTS);
if (length == -1) {
if (_log.isWarnEnabled()) {
_log.warn("Missing />");
}
outputOpenTag(oldCharBuffer, writer, _MARKER_INPUT_OPEN);
return;
}
length += _MARKER_INPUT_CLOSE.length();
String content = extractContent(oldCharBuffer, length);
writer.write(content);
skipWhiteSpace(oldCharBuffer, writer, true);
}
protected void processJavaScript(
String resourceName, CharBuffer charBuffer, Writer writer,
char[] openTag)
throws Exception {
int endPos = openTag.length + 1;
char c = charBuffer.charAt(openTag.length);
if (c == CharPool.SPACE) {
int startPos = openTag.length + 1;
for (int i = startPos; i < charBuffer.length(); i++) {
c = charBuffer.charAt(i);
if (c == CharPool.GREATER_THAN) {
// Open script tag complete
endPos = i + 1;
int length = i - startPos;
if ((length < _MARKER_TYPE_JAVASCRIPT.length()) ||
(KMPSearch.search(
charBuffer, startPos, length,
_MARKER_TYPE_JAVASCRIPT,
_MARKER_TYPE_JAVASCRIPT_NEXTS) == -1)) {
// We have just determined that this is an open script
// tag that does not have the attribute
// type="text/javascript". Now check to see if it has
// the attribute language="JavaScript". If it does not,
// then we skip stripping.
if (!hasLanguageAttribute(
charBuffer, startPos, length)) {
return;
}
}
// Open script tag has no attribute or has attribute
// type="text/javascript". Start stripping.
break;
}
else if (c == CharPool.LESS_THAN) {
// Illegal open script tag. Found a '<' before seeing a '>'.
return;
}
}
if (endPos == charBuffer.length()) {
// Illegal open script tag. Unable to find a '>'.
return;
}
}
else if (c != CharPool.GREATER_THAN) {
// Illegal open script tag. Not followed by a '>' or a ' '.
return;
}
writer.append(charBuffer, 0, endPos);
charBuffer.position(charBuffer.position() + endPos);
int length = KMPSearch.search(
charBuffer, _MARKER_SCRIPT_CLOSE, _MARKER_SCRIPT_CLOSE_NEXTS);
if (length == -1) {
if (_log.isWarnEnabled()) {
_log.warn("Missing </script>");
}
return;
}
if (length == 0) {
outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
return;
}
String content = extractContent(charBuffer, length);
String minifiedContent = content;
if (PropsValues.MINIFIER_INLINE_CONTENT_CACHE_SIZE > 0) {
CacheKeyGenerator cacheKeyGenerator =
CacheKeyGeneratorUtil.getCacheKeyGenerator(
StripFilter.class.getName());
String key = String.valueOf(cacheKeyGenerator.getCacheKey(content));
minifiedContent = _minifierCache.get(key);
if (minifiedContent == null) {
minifiedContent = MinifierUtil.minifyJavaScript(
resourceName, content);
boolean skipCache = false;
for (String skipJavaScript :
PropsValues.
MINIFIER_INLINE_CONTENT_CACHE_SKIP_JAVASCRIPT) {
if (minifiedContent.contains(skipJavaScript)) {
skipCache = true;
break;
}
}
if (!skipCache) {
_minifierCache.put(key, minifiedContent);
}
}
}
if (Validator.isNotNull(minifiedContent)) {
writer.write(minifiedContent);
}
outputCloseTag(charBuffer, writer, _MARKER_SCRIPT_CLOSE);
}
protected void processPre(CharBuffer oldCharBuffer, Writer writer)
throws Exception {
int length = KMPSearch.search(
oldCharBuffer, _MARKER_PRE_OPEN.length + 1, _MARKER_PRE_CLOSE,
_MARKER_PRE_CLOSE_NEXTS);
if (length == -1) {
if (_log.isWarnEnabled()) {
_log.warn("Missing </pre>");
}
outputOpenTag(oldCharBuffer, writer, _MARKER_PRE_OPEN);
return;
}
length += _MARKER_PRE_CLOSE.length();
String content = extractContent(oldCharBuffer, length);
writer.write(content);
skipWhiteSpace(oldCharBuffer, writer, true);
}
protected void processTextArea(CharBuffer oldCharBuffer, Writer writer)
throws Exception {
int length = KMPSearch.search(
oldCharBuffer, _MARKER_TEXTAREA_OPEN.length + 1,
_MARKER_TEXTAREA_CLOSE, _MARKER_TEXTAREA_CLOSE_NEXTS);
if (length == -1) {
if (_log.isWarnEnabled()) {
_log.warn("Missing </textArea>");
}
outputOpenTag(oldCharBuffer, writer, _MARKER_TEXTAREA_OPEN);
return;
}
length += _MARKER_TEXTAREA_CLOSE.length();
String content = extractContent(oldCharBuffer, length);
writer.write(content);
skipWhiteSpace(oldCharBuffer, writer, true);
}
protected boolean skipWhiteSpace(
CharBuffer charBuffer, Writer writer, boolean appendSeparator)
throws Exception {
boolean skipped = false;
for (int i = charBuffer.position(); i < charBuffer.limit(); i++) {
char c = charBuffer.get();
if ((c == CharPool.SPACE) || (c == CharPool.TAB) ||
(c == CharPool.RETURN) || (c == CharPool.NEW_LINE)) {
skipped = true;
continue;
}
else {
charBuffer.position(i);
break;
}
}
if (skipped && appendSeparator) {
writer.write(CharPool.SPACE);
}
return skipped;
}
protected void strip(
HttpServletRequest request, HttpServletResponse response,
CharBuffer charBuffer, Writer writer)
throws Exception {
skipWhiteSpace(charBuffer, writer, false);
while (charBuffer.hasRemaining()) {
char c = charBuffer.get();
writer.write(c);
if (c == CharPool.LESS_THAN) {
if (hasMarker(charBuffer, _MARKER_INPUT_OPEN)) {
processInput(charBuffer, writer);
continue;
}
else if (hasMarker(charBuffer, _MARKER_PRE_OPEN)) {
processPre(charBuffer, writer);
continue;
}
else if (hasMarker(charBuffer, _MARKER_TEXTAREA_OPEN)) {
processTextArea(charBuffer, writer);
continue;
}
else if (hasMarker(charBuffer, _MARKER_SCRIPT_OPEN)) {
StringBuffer requestURL = request.getRequestURL();
processJavaScript(
requestURL.toString(), charBuffer, writer,
_MARKER_SCRIPT_OPEN);
continue;
}
else if (hasMarker(charBuffer, _MARKER_STYLE_OPEN)) {
processCSS(request, response, charBuffer, writer);
continue;
}
}
else if (c == CharPool.GREATER_THAN) {
skipWhiteSpace(charBuffer, writer, true);
}
skipWhiteSpace(charBuffer, writer, true);
}
writer.flush();
}
private static final String _ENSURE_CONTENT_LENGTH = "ensureContentLength";
private static final String _MARKER_INPUT_CLOSE = "/>";
private static final int[] _MARKER_INPUT_CLOSE_NEXTS =
KMPSearch.generateNexts(_MARKER_INPUT_CLOSE);
private static final char[] _MARKER_INPUT_OPEN = "input".toCharArray();
private static final String _MARKER_LANGUAGE = "language=";
private static final int[] _MARKER_LANGUAGE_NEXTS = KMPSearch.generateNexts(
_MARKER_LANGUAGE);
private static final String _MARKER_PRE_CLOSE = "/pre>";
private static final int[] _MARKER_PRE_CLOSE_NEXTS =
KMPSearch.generateNexts(_MARKER_PRE_CLOSE);
private static final char[] _MARKER_PRE_OPEN = "pre".toCharArray();
private static final String _MARKER_SCRIPT_CLOSE = "</script>";
private static final int[] _MARKER_SCRIPT_CLOSE_NEXTS =
KMPSearch.generateNexts(_MARKER_SCRIPT_CLOSE);
private static final char[] _MARKER_SCRIPT_OPEN = "script".toCharArray();
private static final String _MARKER_STYLE_CLOSE = "</style>";
private static final int[] _MARKER_STYLE_CLOSE_NEXTS =
KMPSearch.generateNexts(_MARKER_STYLE_CLOSE);
private static final char[] _MARKER_STYLE_OPEN =
"style type=\"text/css\">".toCharArray();
private static final String _MARKER_TEXTAREA_CLOSE = "/textarea>";
private static final int[] _MARKER_TEXTAREA_CLOSE_NEXTS =
KMPSearch.generateNexts(_MARKER_TEXTAREA_CLOSE);
private static final char[] _MARKER_TEXTAREA_OPEN =
"textarea ".toCharArray();
private static final String _MARKER_TYPE_JAVASCRIPT =
"type=\"text/javascript\"";
private static final int[] _MARKER_TYPE_JAVASCRIPT_NEXTS =
KMPSearch.generateNexts(_MARKER_TYPE_JAVASCRIPT);
private static final String _STRIP = "strip";
private static final Log _log = LogFactoryUtil.getLog(StripFilter.class);
private static final Pattern _javaScriptPattern = Pattern.compile(
"[Jj][aA][vV][aA][sS][cC][rR][iI][pP][tT]");
private final Set<String> _ignorePaths = new HashSet<>();
private final ConcurrentLFUCache<String, String> _minifierCache;
}