AssetRenditionContentDispositionFilter.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2018 wcm.io
 * %%
 * Licensed 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.
 * #L%
 */
package io.wcm.handler.media.impl;

import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.day.cq.commons.jcr.JcrConstants.JCR_MIMETYPE;
import static com.day.cq.commons.jcr.JcrConstants.JCR_PRIMARYTYPE;
import static com.day.cq.commons.jcr.JcrConstants.NT_FILE;
import static io.wcm.handler.media.impl.MediaFileServletConstants.HEADER_CONTENT_DISPOSITION;
import static org.apache.sling.api.servlets.HttpConstants.METHOD_GET;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

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.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;

/**
 * Servlet filter that applies the logic of the AEM "Dam Safe Binary Filter" also to direct
 * references to renditions at <code>{asset-path}/jcr_content/renditions/*</code>.
 * It re-used the configuration of the "Dam Safe Binary Filter"
 * (com.day.cq.dam.core.impl.servlet.DamContentDispositionFilter),
 * so both filters have the same result.
 * <p>
 * Unlike for Asset paths where the "Dam Safe Binary Filter" applies for rendition paths the
 * "Sling Content Disposition Filter" is applied first which adds a "attachment" content disposition header for all
 * paths that are not whitelisted. This filter resets this to an "inline" content disposition header for all mime types
 * that are not blacklisted.
 * </p>
 */
@Component(service = Filter.class,
    name = "com.day.cq.dam.core.impl.servlet.DamContentDispositionFilter", // reuse config from 'Dam Safe Binary Filter'
    property = {
        "sling.filter.scope=request",
        "sling.filter.pattern=/content/dam/.*/(jcr:content|_jcr_content)/renditions/.*",
        "service.ranking=-25001"
})
public final class AssetRenditionContentDispositionFilter implements Filter {

  static final String BLACK_LIST_MIME_TYPE_CONFIG = "cq.mime.type.blacklist";
  static final String ALLOW_EMPTY_MIME = "cq.dam.empty.mime";

  private Set<String> mimetypeBlacklist;
  private boolean allowEmptyMime;

  @Activate
  private void activate(Map<String, Object> config) {
    String[] mimetypeBlacklistArray = PropertiesUtil.toStringArray(config.get(BLACK_LIST_MIME_TYPE_CONFIG));
    if (mimetypeBlacklistArray != null) {
      mimetypeBlacklist = Arrays.stream(mimetypeBlacklistArray)
          .map(StringUtils::lowerCase)
          .collect(Collectors.toSet());
    }
    else {
      mimetypeBlacklist = Collections.emptySet();
    }
    allowEmptyMime = PropertiesUtil.toBoolean(config.get(ALLOW_EMPTY_MIME), false);
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    // do nothing
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
    SlingHttpServletResponse slingResponse = (SlingHttpServletResponse)response;
    if (accepts(slingRequest)) {
      setContentDisposition(slingRequest, slingResponse);
    }
    filterChain.doFilter(request, response);
  }

  @SuppressWarnings("null")
  private void setContentDisposition(SlingHttpServletRequest request, SlingHttpServletResponse response) {
    Resource resource = request.getResource();

    // get mimetype from nt:file resource
    String mimeType = resource.getValueMap().get(JCR_CONTENT + "/" + JCR_MIMETYPE, String.class);

    // if mimetype is not blacklisted, or empty (and this is allowed) send "inline" content disposition header
    if ((StringUtils.isNotBlank(mimeType) && !mimetypeBlacklist.contains(mimeType.toLowerCase()))
        || (StringUtils.isBlank(mimeType) && allowEmptyMime)) {
      response.setHeader(HEADER_CONTENT_DISPOSITION, "inline");
    }
  }

  /**
   * This filter only processes GET requests that targets a nt:file resource.
   * @param request Request
   * @return true if the filter accepts the given request
   */
  @SuppressWarnings("null")
  private boolean accepts(SlingHttpServletRequest request) {
    return StringUtils.equalsIgnoreCase(request.getMethod(), METHOD_GET)
        && request.getResource() != null
        && StringUtils.equals(request.getResource().getValueMap().get(JCR_PRIMARYTYPE, String.class), NT_FILE);
  }

  @Override
  public void destroy() {
    // do nothing
  }

}