All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.faces.application.resource.ResourceHandlerImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1997, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.faces.application.resource;

import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.DefaultResourceMaxAge;
import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.ResourceBufferSize;
import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.ResourceExcludes;
import static com.sun.faces.util.RequestStateManager.RESOURCE_REQUEST;
import static com.sun.faces.util.Util.getFacesMapping;
import static com.sun.faces.util.Util.notNegative;
import static com.sun.faces.util.Util.notNull;
import static jakarta.faces.application.ProjectStage.Development;
import static jakarta.faces.application.ProjectStage.Production;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static jakarta.servlet.http.MappingMatch.EXTENSION;
import static java.lang.Boolean.FALSE;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import com.sun.faces.application.ApplicationAssociate;
import com.sun.faces.config.WebConfiguration;
import com.sun.faces.util.FacesLogger;
import com.sun.faces.util.RequestStateManager;
import com.sun.faces.util.Util;

import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.application.ResourceVisitOption;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;

/**
 * This is the default implementation of {@link ResourceHandler}.
 */
public class ResourceHandlerImpl extends ResourceHandler {

    // Log instance for this class
    private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger();

    ResourceManager manager;
    List excludePatterns;
    private long creationTime;
    private long maxAge;
    private final WebConfiguration webconfig;

    // ------------------------------------------------------------ Constructors

    /**
     * Creates a new instance of ResourceHandlerImpl
     */
    public ResourceHandlerImpl() {
        creationTime = System.currentTimeMillis();
        webconfig = WebConfiguration.getInstance();
        ExternalContext extContext = FacesContext.getCurrentInstance().getExternalContext();
        manager = ApplicationAssociate.getInstance(extContext).getResourceManager();
        initExclusions(extContext.getApplicationMap());
        initMaxAge();
    }

    // ------------------------------------------- Methods from Resource Handler

    /**
     * @see ResourceHandler#createResource(String)
     */
    @Override
    public Resource createResource(String resourceName) {
        return createResource(resourceName, null, null);
    }

    /**
     * @see ResourceHandler#createResource(String, String)
     */
    @Override
    public Resource createResource(String resourceName, String libraryName) {
        return createResource(resourceName, libraryName, null);
    }

    /**
     * @see ResourceHandler#createResource(String, String, String)
     */
    @Override
    public Resource createResource(String resourceName, String libraryName, String contentType) {
        notNull("resourceName", resourceName);

        FacesContext ctx = FacesContext.getCurrentInstance();

        String ctype = contentType != null ? contentType : getContentType(ctx, resourceName);
        ResourceInfo info = manager.findResource(libraryName, resourceName, ctype, ctx);

        if (info == null) {
            return null;
        }

        return new ResourceImpl(info, ctype, creationTime, maxAge);
    }

    @Override
    public Resource createViewResource(FacesContext facesContext, String resourceName) {
        notNull("resourceName", resourceName);

        String contentType = getContentType(facesContext, resourceName);
        ResourceInfo resourceInfo = manager.findViewResource(resourceName, contentType, facesContext);

        if (resourceInfo == null) {
            return null;
        }

        return new ResourceImpl(resourceInfo, contentType, creationTime, maxAge);
    }

    /**
     * @see ResourceHandler#getViewResources(FacesContext, String, ResourceVisitOption...)
     */
    @Override
    public Stream getViewResources(FacesContext facesContext, String path, ResourceVisitOption... options) {
        notNull("path", path);

        return manager.getViewResources(facesContext, path, Integer.MAX_VALUE, options);
    }

    /**
     * @see ResourceHandler#getViewResources(FacesContext, String, int, ResourceVisitOption...)
     */
    @Override
    public Stream getViewResources(FacesContext facesContext, String path, int maxDepth, ResourceVisitOption... options) {
        notNull("path", path);
        notNegative("maxDepth", maxDepth);

        return manager.getViewResources(facesContext, path, maxDepth, options);
    }

    /**
     * @see ResourceHandler#createResourceFromId(String)
     */
    @Override
    public Resource createResourceFromId(String resourceId) {
        notNull("resourceId", resourceId);
        FacesContext ctx = FacesContext.getCurrentInstance();

        boolean development = ctx.isProjectStage(Development);

        ResourceInfo info = manager.findResource(resourceId);
        String ctype = getContentType(ctx, resourceId);
        if (info == null) {
            logMissingResource(ctx, resourceId, null);
            return null;
        } else {
            return new ResourceImpl(info, ctype, creationTime, maxAge);
        }

    }

    @Override
    public boolean libraryExists(String libraryName) {

        if (libraryName.contains("../")) {
            return false;
        }

        FacesContext context = FacesContext.getCurrentInstance();
        // PENDING(fcaputo) do we need to iterate over the contracts here? I don't think so.
        LibraryInfo info = manager.findLibrary(libraryName, null, null, context);
        if (info == null) {
            info = manager.findLibraryOnClasspathWithZipDirectoryEntryScan(libraryName, null, null, context, true);

        }

        return info != null;
    }

    /**
     * @see ResourceHandler#isResourceRequest(jakarta.faces.context.FacesContext)
     */
    @Override
    public boolean isResourceRequest(FacesContext context) {

        Boolean isResourceRequest = RequestStateManager.get(context, RESOURCE_REQUEST);
        if (isResourceRequest == null) {
            String resourceId = normalizeResourceRequest(context);
            isResourceRequest = resourceId != null ? resourceId.startsWith(RESOURCE_IDENTIFIER) : FALSE;
            RequestStateManager.set(context, RESOURCE_REQUEST, isResourceRequest);
        }

        return isResourceRequest;
    }

    @Override
    public String getRendererTypeForResourceName(String resourceName) {
        String rendererType = null;

        String contentType = getContentType(FacesContext.getCurrentInstance(), resourceName);
        if (null != contentType) {
            contentType = contentType.toLowerCase();
            if (contentType.contains("javascript")) {
                rendererType = "jakarta.faces.resource.Script";
            } else if (contentType.contains("css")) {
                rendererType = "jakarta.faces.resource.Stylesheet";
            }
        }

        return rendererType;
    }

    /**
     * @see jakarta.faces.application.ResourceHandler#handleResourceRequest(jakarta.faces.context.FacesContext)
     */
    @Override
    public void handleResourceRequest(FacesContext context) throws IOException {
        String resourceId = normalizeResourceRequest(context);
        if (resourceId == null) {
            // handleResourceRequest called for a non-resource request, bail out.
            return;
        }

        ExternalContext extContext = context.getExternalContext();

        if (isExcluded(resourceId)) {
            extContext.setResponseStatus(SC_NOT_FOUND);
            return;
        }

        assert null != resourceId;
        assert resourceId.startsWith(RESOURCE_IDENTIFIER);

        Resource resource = null;
        String resourceName = null;
        String libraryName = null;
        if (RESOURCE_IDENTIFIER.length() < resourceId.length()) {
            resourceName = resourceId.substring(RESOURCE_IDENTIFIER.length() + 1);
            assert resourceName != null;
            libraryName = context.getExternalContext().getRequestParameterMap().get("ln");

            boolean createResource;

            if (libraryName != null) {
                createResource = libraryNameIsSafe(libraryName);
                if (!createResource) {
                    send404(context, resourceName, libraryName, true);
                    return;
                }
            } else {
                createResource = true;
            }
            if (createResource) {
                resource = context.getApplication().getResourceHandler().createResource(resourceName, libraryName);
            }
        }

        if (resource != null) {
            if (resource.userAgentNeedsUpdate(context)) {
                ReadableByteChannel resourceChannel = null;
                WritableByteChannel out = null;
                ByteBuffer buf = allocateByteBuffer();
                try {
                    InputStream in = resource.getInputStream();
                    if (in == null) {
                        send404(context, resourceName, libraryName, true);
                        return;
                    }
                    resourceChannel = Channels.newChannel(in);
                    out = Channels.newChannel(extContext.getResponseOutputStream());
                    extContext.setResponseBufferSize(buf.capacity());
                    String contentType = resource.getContentType();
                    if (contentType != null) {
                        extContext.setResponseContentType(resource.getContentType());
                    }
                    handleHeaders(context, resource);

                    int size = 0;
                    for (int thisRead = resourceChannel.read(buf), totalWritten = 0; thisRead != -1; thisRead = resourceChannel.read(buf)) {

                        buf.rewind();
                        buf.limit(thisRead);
                        size += thisRead;
                        do {
                            totalWritten += out.write(buf);
                        } while (totalWritten < size);
                        buf.clear();
                    }

                    if (!extContext.isResponseCommitted()) {
                        extContext.setResponseContentLength(size);
                    }

                } catch (IOException ioe) {
                    if (isConnectionAbort(ioe)) { // to be removed, when the exception is standardised in servlet.
                        send404(context, resourceName, libraryName, false);
                    } else {
                        send404(context, resourceName, libraryName, ioe, true);
                    }
                } finally {
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException ignored) {
                            // Maybe log a warning here?
                        }
                    }
                    if (resourceChannel != null) {
                        resourceChannel.close();
                    }
                }
            } else {
                send304(context);
            }

        } else {
            // already logged elsewhere
            send404(context, resourceName, libraryName, true);
        }

    }

    private static boolean isConnectionAbort(IOException ioe) {
        if (ioe instanceof ClosedChannelException) {
            return true;
        }

        String exceptionClassName = ioe.getClass().getCanonicalName();

        if (exceptionClassName.equals("org.apache.catalina.connector.ClientAbortException") ||
                exceptionClassName.equals("org.eclipse.jetty.io.EofException")) {
            return true;
        }

        String exceptionMessage = ioe.getMessage();

        if (exceptionMessage == null) {
            return false;
        }

        String lowercasedExceptionMessage = exceptionMessage.toLowerCase();
        return lowercasedExceptionMessage.contains("connection") && lowercasedExceptionMessage.contains("abort"); // #5264
    }

    private boolean libraryNameIsSafe(String libraryName) {
        assert null != libraryName;
        boolean result;

        result = !(libraryName.startsWith(".") ||

                libraryName.startsWith("/") || libraryName.contains("/") ||

                libraryName.startsWith("\\") || libraryName.contains("\\") ||

                libraryName.startsWith("%2e") ||

                libraryName.startsWith("%2f") || libraryName.contains("%2f") ||

                libraryName.startsWith("%5c") || libraryName.contains("%5c") ||

                libraryName.startsWith("\\u002e") ||

                libraryName.startsWith("\\u002f") || libraryName.contains("\\u002f") ||

                libraryName.startsWith("\\u005c") || libraryName.contains("\\u005c"));

        return result;
    }

    private void send404(FacesContext ctx, String resourceName, String libraryName, boolean logMessage) {
        send404(ctx, resourceName, libraryName, null, logMessage);
    }

    private void send404(FacesContext ctx, String resourceName, String libraryName, Throwable t, boolean logMessage) {
        ctx.getExternalContext().setResponseStatus(SC_NOT_FOUND);
        if (logMessage) {
            logMissingResource(ctx, resourceName, libraryName, t);
        }
    }

    private void send304(FacesContext ctx) {
        ctx.getExternalContext().setResponseStatus(SC_NOT_MODIFIED);
    }

    // ------------------------------------------------- Package Private Methods

    /**
     * This method is leveraged by {@link ResourceImpl} to detemine if a resource has been upated. In short, a resource has
     * been updated if the timestamp is newer than the timestamp of the ResourceHandler creation time.
     *
     * @return the time when the ResourceHandler was instantiated (in milliseconds)
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    long getCreationTime() {
        return creationTime;
    }

    /**
     * This method is here soley for the purpose of unit testing and will not be invoked during normal runtime.
     *
     * @param creationTime the time in milliseconds
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    void setCreationTime(long creationTime) {
        this.creationTime = creationTime;
    }

    /**
     * Utility method leveraged by ResourceImpl to reduce the cost of looking up the WebConfiguration per-instance.
     *
     * @return the {@link WebConfiguration} for this application
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    WebConfiguration getWebConfig() {
        return webconfig;
    }

    // --------------------------------------------------------- Private Methods

    /**
     * Log a message indicating a particular resource (reference by name and/or library) could not be found. If this was due
     * to an exception, the exception provided will be logged as well.
     *
     * @param ctx the {@link FacesContext} for the current request
     * @param resourceName the resource name
     * @param libraryName the resource library
     * @param t the exception caught when attempting to find the resource
     */
    private void logMissingResource(FacesContext ctx, String resourceName, String libraryName, Throwable t) {

        Level level;
        if (!ctx.isProjectStage(Production)) {
            level = WARNING;
        } else {
            level = t != null ? WARNING : FINE;
        }

        if (libraryName != null) {
            if (LOGGER.isLoggable(level)) {
                LOGGER.log(level, "faces.application.resource.unable_to_serve_from_library", new Object[] { resourceName, libraryName });
                if (t != null) {
                    LOGGER.log(level, "", t);
                }
            }
        } else {
            if (LOGGER.isLoggable(level)) {
                LOGGER.log(level, "faces.application.resource.unable_to_serve", new Object[] { resourceName });
                if (t != null) {
                    LOGGER.log(level, "", t);
                }
            }
        }

    }

    /**
     * Log a message indicating a particular resource (reference by name and/or library) could not be found. If this was due
     * to an exception, the exception provided will be logged as well.
     *
     * @param ctx the {@link FacesContext} for the current request
     * @param resourceName the resource name
     * @param libraryName the resource library
     * @param t the exception caught when attempting to find the resource
     */
    private void logMissingResource(FacesContext ctx, String resourceId, Throwable t) {
        Level level;
        if (!ctx.isProjectStage(Production)) {
            level = WARNING;
        } else {
            level = t != null ? WARNING : FINE;
        }

        if (LOGGER.isLoggable(level)) {
            LOGGER.log(level, "faces.application.resource.unable_to_serve", new Object[] { resourceId });
            if (t != null) {
                LOGGER.log(level, "", t);
            }
        }

    }

    /**
     * @param resourceName the resource of interest. The resourceName in question may consist of zero or more path elements
     * such that resourceName could be something like path1/path2/resource.jpg or resource.jpg
     * @return the content type for this resource
     */
    private String getContentType(FacesContext ctx, String resourceName) {
        return ctx.getExternalContext().getMimeType(resourceName);
    }

    /**
     * Normalize the request path to exclude Faces invocation information.
     *
     * 

* If the FacesServlet servicing this request was * extension mapped, then the extension will be trimmed off. * *

* If the FacesServlet servicing this request was * prefix mapped, then the path to the FacesServlet will be removed. * * @param context the FacesContext for the current request * @return the request path without Faces invocation information */ private String normalizeResourceRequest(FacesContext context) { // If it is extension mapped if (getFacesMapping(context).getMappingMatch() == EXTENSION) { String path = context.getExternalContext().getRequestServletPath(); // strip off the extension return path.substring(0, path.lastIndexOf(".")); } return context.getExternalContext().getRequestPathInfo(); } /** * @param resourceId the normalized request path as returned by * {@link #normalizeResourceRequest(jakarta.faces.context.FacesContext)} * @return true if the request matces an excluded resource, otherwise false */ private boolean isExcluded(String resourceId) { for (Pattern pattern : excludePatterns) { if (pattern.matcher(resourceId).matches()) { return true; } } return false; } /** * Initialize the exclusions for this application. If no explicit exclusions are configured, the defaults of *

    *
  • .class
  • *
  • .properties
  • *
  • .xhtml
  • *
      * will be used. */ private void initExclusions(Map appMap) { String excludesParam = webconfig.getOptionValue(ResourceExcludes); String[] patterns = Util.split(appMap, excludesParam, " "); excludePatterns = new ArrayList<>(patterns.length); for (String pattern : patterns) { excludePatterns.add(Pattern.compile(".*\\" + pattern)); } } private void initMaxAge() { maxAge = Long.parseLong(webconfig.getOptionValue(DefaultResourceMaxAge)); } private void handleHeaders(FacesContext ctx, Resource resource) { ExternalContext extContext = ctx.getExternalContext(); for (Map.Entry cur : resource.getResponseHeaders().entrySet()) { extContext.setResponseHeader(cur.getKey(), cur.getValue()); } } private ByteBuffer allocateByteBuffer() { int size; try { size = Integer.parseInt(webconfig.getOptionValue(ResourceBufferSize)); } catch (NumberFormatException nfe) { if (LOGGER.isLoggable(WARNING)) { LOGGER.log(WARNING, "faces.application.resource.invalid_resource_buffer_size", new Object[] { webconfig.getOptionValue(ResourceBufferSize), ResourceBufferSize.getQualifiedName(), ResourceBufferSize.getDefaultValue() }); } size = Integer.parseInt(ResourceBufferSize.getDefaultValue()); } return ByteBuffer.allocate(size); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy