/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.security.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(property={"sling.filter.scope=request", "service.ranking:Integer=25000"})
@Designate(ocd=ContentDispositionFilterConfiguration.class)
public class ContentDispositionFilter implements Filter {
/** Logger. */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Set of paths
*/
Set<String> contentDispositionPaths;
/**
* Array of prefixes of paths
*/
private String[] contentDispositionPathsPfx;
Set<String> contentDispositionExcludedPaths;
private Map<String, Set<String>> contentTypesMapping;
private boolean enableContentDispositionAllPaths;
@Activate
private void activate(ContentDispositionFilterConfiguration configuration) {
String[] contentDispositionPathsConfiguredValue = configuration.sling_content_disposition_paths();
Set<String> paths = new HashSet<String>();
List<String> pfxs = new ArrayList<String>();
Map<String, Set<String>> contentTypesMap = new HashMap<String, Set<String>>();
for (String path : contentDispositionPathsConfiguredValue) {
path = path.trim();
if (path.length() > 0) {
int idx = path.indexOf('*');
int colonIdx = path.indexOf(":");
if (colonIdx > -1 && colonIdx < idx) {
// ':' in paths is not allowed
logger.info("wildcard ('*') in content type is not allowed, but found content type with value '{}'", path.substring(colonIdx));
} else {
String p = null;
if (idx >= 0) {
if (idx > 0) {
p = path.substring(0, idx);
pfxs.add(p);
} else {
// we don't allow "*" - that would defeat the
// purpose.
logger.info("catch-all wildcard for paths not allowed.");
}
} else {
if (colonIdx > -1) {
p = path.substring(0, colonIdx);
} else {
p = path;
}
paths.add(p);
}
if (colonIdx != -1 && p != null) {
Set <String> contentTypes = getContentTypes(path.substring(colonIdx+1));
contentTypesMap.put(p, contentTypes);
}
}
}
}
contentDispositionPaths = paths.isEmpty() ? Collections.<String>emptySet() : paths;
contentDispositionPathsPfx = pfxs.toArray(new String[pfxs.size()]);
contentTypesMapping = contentTypesMap.isEmpty()?Collections.<String, Set<String>>emptyMap(): contentTypesMap;
enableContentDispositionAllPaths = configuration.sling_content_disposition_all_paths();
String[] contentDispostionExcludedPathsArray = configuration.sling_content_disposition_excluded_paths();
contentDispositionExcludedPaths = new HashSet<String>(Arrays.asList(contentDispostionExcludedPathsArray));
logger.info("Initialized. content disposition paths: {}, content disposition paths-pfx {}, content disposition excluded paths: {}. Enable Content Disposition for all paths is set to {}", new Object[]{
contentDispositionPaths, contentDispositionPathsPfx, contentDispositionExcludedPaths, enableContentDispositionAllPaths}
);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// nothing to do
}
@Override
public void destroy() {
// nothing to do
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
final SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;
final RewriterResponse rewriterResponse = new RewriterResponse(slingRequest, slingResponse);
chain.doFilter(request, rewriterResponse);
}
//---------- PRIVATE METHODS ---------
private static Set<String> getContentTypes(String contentTypes) {
Set<String> contentTypesSet = new HashSet<String>();
if (contentTypes != null && contentTypes.length() > 0) {
String[] contentTypesArray = contentTypes.split(",");
for (String contentType : contentTypesArray) {
contentTypesSet.add(contentType);
}
}
return contentTypesSet;
}
//----------- INNER CLASSES ------------
protected class RewriterResponse extends SlingHttpServletResponseWrapper {
private static final String CONTENT_DISPOSTION = "Content-Disposition";
private static final String CONTENT_DISPOSTION_ATTACHMENT = "attachment";
private static final String PROP_JCR_DATA = "jcr:data";
private static final String JCR_CONTENT_LEAF = "jcr:content";
static final String ATTRIBUTE_NAME =
"org.apache.sling.security.impl.ContentDispositionFilter.RewriterResponse.contentType";
/** The current request. */
private final SlingHttpServletRequest request;
private final Resource resource;
public RewriterResponse(SlingHttpServletRequest request, SlingHttpServletResponse wrappedResponse) {
super(wrappedResponse);
this.request = request;
this.resource = request.getResource();
}
@Override
public void reset() {
request.removeAttribute(ATTRIBUTE_NAME);
super.reset();
}
/**
* @see javax.servlet.ServletResponseWrapper#setContentType(java.lang.String)
*/
@Override
public void setContentType(String type) {
if ("GET".equals(request.getMethod())) {
String previousContentType = (String) request.getAttribute(ATTRIBUTE_NAME);
if (previousContentType != null && previousContentType.equals(type)) {
return;
}
request.setAttribute(ATTRIBUTE_NAME, type);
String resourcePath = resource.getPath();
if (!contentDispositionExcludedPaths.contains(resourcePath)) {
if (enableContentDispositionAllPaths) {
setContentDisposition(resource);
} else {
boolean contentDispositionAdded = false;
if (contentDispositionPaths.contains(resourcePath)) {
if (contentTypesMapping.containsKey(resourcePath)) {
Set <String> exceptions = contentTypesMapping.get(resourcePath);
if (!exceptions.contains(type)) {
contentDispositionAdded = setContentDisposition(resource);
}
} else {
contentDispositionAdded = setContentDisposition(resource);
}
}
if (!contentDispositionAdded) {
for (String path : contentDispositionPathsPfx) {
if (resourcePath.startsWith(path)) {
if (contentTypesMapping.containsKey(path)) {
Set <String> exceptions = contentTypesMapping.get(path);
if (!exceptions.contains(type)) {
setContentDisposition(resource);
break;
}
} else {
setContentDisposition(resource);
break;
}
}
}
}
}
}
}
super.setContentType(type);
}
//---------- PRIVATE METHODS ---------
private boolean setContentDisposition(Resource resource) {
boolean contentDispositionAdded = false;
if (!this.containsHeader(CONTENT_DISPOSTION) && this.isJcrData(resource)) {
this.addHeader(CONTENT_DISPOSTION, CONTENT_DISPOSTION_ATTACHMENT);
contentDispositionAdded = true;
}
return contentDispositionAdded;
}
private boolean isJcrData(Resource resource){
boolean jcrData = false;
if (resource!= null) {
ValueMap props = resource.adaptTo(ValueMap.class);
if (props != null && props.containsKey(PROP_JCR_DATA) ) {
jcrData = true;
} else {
Resource jcrContent = resource.getChild(JCR_CONTENT_LEAF);
if (jcrContent!= null) {
props = jcrContent.adaptTo(ValueMap.class);
if (props != null && props.containsKey(PROP_JCR_DATA) ) {
jcrData = true;
}
}
}
}
return jcrData;
}
}
}