/* * 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.servlets.get.impl; import java.io.IOException; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestPathInfo; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.apache.sling.servlets.get.impl.helpers.JsonRendererServlet; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.Designate; import org.osgi.service.metatype.annotations.ObjectClassDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>RedirectServlet</code> implements support for GET requests to * resources of type <code>sling:redirect</code>. This servlet tries to get the * redirect target by * <ul> * <li>first adapting the resource to a {@link ValueMap} and trying to get the * property <code>sling:target</code>.</li> * <li>The second attempt is to access the resource <code>sling:target</code> * below the requested resource and attapt this to a string.</li> * </ul> * <p> * If there is no value found for <code>sling:target</code> a 404 (NOT FOUND) * status is sent by this servlet. Otherwise a 302 (FOUND, temporary redirect) * status is sent where the target is the relative URL from the current resource * to the target resource. Selectors, extension, suffix and query string are * also appended to the redirect URL. */ @SuppressWarnings("serial") @Component(service = Servlet.class, property = { "service.description=Request Redirect Servlet", "service.vendor=The Apache Software Foundation", "sling.servlet.resourceTypes=sling:redirect", "sling.servlet.methods=GET", "sling.servlet.prefix:Integer=-1" }) @Designate(ocd = RedirectServlet.Config.class) public class RedirectServlet extends SlingSafeMethodsServlet { @ObjectClassDefinition(name="Apache Sling Redirect Servlet", description="The Sling servlet handling redirect resources.") public @interface Config { @AttributeDefinition(name = "JSON Max results", description = "The maximum number of resources that should " + "be returned when doing a node.5.json or node.infinity.json. In JSON terms " + "this basically means the number of Objects to return. Default value is " + "200.") int json_maximumresults() default 200; } /** The name of the target property */ public static final String TARGET_PROP = "sling:target"; /** The name of the redirect status property */ public static final String STATUS_PROP = "sling:status"; /** default log */ private final Logger log = LoggerFactory.getLogger(getClass()); private Servlet jsonRendererServlet; private int jsonMaximumResults; @Activate protected void activate(Config cfg) { this.jsonMaximumResults = cfg.json_maximumresults(); // When the maximumResults get updated, we force a reset for the jsonRendererServlet. jsonRendererServlet = getJsonRendererServlet(); } @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { // handle json export of the redirect node if (JsonRendererServlet.EXT_JSON.equals(request.getRequestPathInfo().getExtension())) { getJsonRendererServlet().service(request, response); return; } // check for redirectability if (response.isCommitted()) { // committed response cannot be redirected log.warn("RedirectServlet: Response is already committed, not redirecting"); request.getRequestProgressTracker().log( "RedirectServlet: Response is already committed, not redirecting"); return; } else if (request.getAttribute(SlingConstants.ATTR_REQUEST_SERVLET) != null) { // included request will not redirect log.warn("RedirectServlet: Servlet is included, not redirecting"); request.getRequestProgressTracker().log( "RedirectServlet: Servlet is included, not redirecting"); return; } String targetPath = null; // convert resource to a value map final Resource rsrc = request.getResource(); final ValueMap valueMap = rsrc.adaptTo(ValueMap.class); if (valueMap != null) { targetPath = valueMap.get(TARGET_PROP, String.class); } if (targetPath == null) { // old behaviour final Resource targetResource = request.getResourceResolver().getResource( rsrc, TARGET_PROP); if (targetResource == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Missing target for redirection"); return; } // if the target resource is a path (string), redirect there targetPath = targetResource.adaptTo(String.class); } // if we got a target path, make it external and redirect to it if (targetPath != null) { if (!isUrl(targetPath)) { // make path relative and append selectors, extension etc. // this is an absolute URI suitable for the Location header targetPath = toRedirectPath(targetPath, request); } else { // just append any selectors, extension, suffix and query string targetPath = appendSelectorsExtensionSuffixQuery(request, new StringBuilder(targetPath)).toString(); } final int status = getStatus(valueMap); // redirect the client, use our own setup since we might have a // custom response status and we already have converted the target // into an absolute URI. response.reset(); response.setStatus(status); response.setHeader("Location", isUrl(targetPath) ? targetPath : response.encodeRedirectURL(targetPath)); response.flushBuffer(); return; } // no way of finding the target, just fail response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot redirect to target resource " + targetPath); } /** * Returns the response status from the {@link #STATUS_PROP} property in the * value map. If <code>valueMap</code> is <code>null</code>, the property is * not contained in the map or if the value is outside of the value HTTP * response status range of [ 100 .. 999 ], the default status 302/FOUND is * returned. * * @param valueMap The <code>valueMap</code> providing the optional status * property. * @return The status value as defined above. */ static int getStatus(final ValueMap valueMap) { if (valueMap != null) { final Integer statusInt = valueMap.get(STATUS_PROP, Integer.class); if (statusInt != null) { int status = statusInt.intValue(); if (status >= 100 && status <= 999) { return status; } } } // fall back to default value return HttpServletResponse.SC_FOUND; } /** * Create an absolute URI suitable for the "Location" response header * including any selectors, extension, suffix and query from the current * request. */ static String toRedirectPath(String targetPath, SlingHttpServletRequest request) { // make sure the target path is absolute final String rawAbsPath; if (targetPath.startsWith("/")) { rawAbsPath = targetPath; } else { rawAbsPath = request.getResource().getPath() + "/" + targetPath; } final StringBuilder target = new StringBuilder(); // and ensure the path is normalized, us unnormalized if not possible final String absPath = ResourceUtil.normalize(rawAbsPath); if (absPath == null) { target.append(rawAbsPath); } else { target.append(absPath); } appendSelectorsExtensionSuffixQuery(request, target); // return the mapped full path return request.getResourceResolver().map(request, target.toString()); } /** * Appends optional request selectors, extension, suffix and query string to * the URL to be prepared in the target string builder and returns the * string builder. * * @param request The Sling HTTP Servlet Request providing access to the * data to be appended * @param target The String builder to append the data to. This must not be * null. * @return The <code>target</code> string builder. * @throws NullPointerException if request or target is <code>null</code>. */ private static StringBuilder appendSelectorsExtensionSuffixQuery( SlingHttpServletRequest request, StringBuilder target) { // append current selectors, extension and suffix final RequestPathInfo rpi = request.getRequestPathInfo(); if (rpi.getExtension() != null) { if (rpi.getSelectorString() != null) { target.append('.').append(rpi.getSelectorString()); } target.append('.').append(rpi.getExtension()); if (rpi.getSuffix() != null) { target.append(rpi.getSuffix()); } } // append current querystring if (request.getQueryString() != null) { target.append('?').append(request.getQueryString()); } return target; } /** * Returns an absolute URI built from the given parameters. * * @param scheme The scheme for the URI to be built. * @param host The name of the host. * @param port The port or -1 to not add a port number to the URI. For * <code>http</code> and <code>https</code> schemes the port is * not added if it is the default port. * @param targetPath The path of the resulting URI. This path is expected to * not be an absolute URI. * @return The absolute URI built from the components. */ static String toAbsoluteUri(final String scheme, final String host, final int port, final String targetPath) { // 1. scheme and host final StringBuilder absUriBuilder = new StringBuilder(); absUriBuilder.append(scheme).append("://").append(host); // 2. append the port depending on the scheme and whether the port is // the default or not if (port > 0) { if (!(("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443))) { absUriBuilder.append(':').append(port); } } // 3. the actual target path absUriBuilder.append(targetPath); return absUriBuilder.toString(); } private Servlet getJsonRendererServlet() { if (jsonRendererServlet == null) { Servlet jrs = new JsonRendererServlet(jsonMaximumResults); try { jrs.init(getServletConfig()); } catch (Exception e) { // don't care too much here } jsonRendererServlet = jrs; } return jsonRendererServlet; } /** * Returns <code>true</code> if the path is potentially an URL. This * checks whether the path starts with a scheme followed by a colon * according to <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC-2396</a>: * <pre> * scheme = alpha *( alpha | digit | "+" | "-" | "." ) * alpha = [ "A" .. "Z", "a" .. "z" ] * digit = [ "0" .. "9" ] * </pre> */ private static boolean isUrl(final String path) { for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); if (c == ':') { return true; } if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (i > 0 && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-')))) { break; } } return false; } }