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

org.eclipse.jetty.server.ResourceListing Maven / Gradle / Ivy

There is a newer version: 12.1.0.alpha0
Show newest version
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollators;
import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility methods to generate a List of paths.
 *
 * TODO: add XML and JSON versions?
 */
public class ResourceListing
{
    public static final Logger LOG = LoggerFactory.getLogger(ResourceListing.class);

    /**
     * Convert the Resource directory into an XHTML directory listing.
     *
     * @param resource the resource to build the listing from
     * @param base The base URL
     * @param parent True if the parent directory should be included
     * @param query query params
     * @return the XHTML as String
     */
    public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
    {
        // This method doesn't check aliases, so it is OK to canonicalize here.
        base = URIUtil.normalizePath(base);
        if (base == null)
            return null;
        if (!Resources.isReadableDirectory(resource))
            return null;

        List listing = resource.list().stream()
            .filter(distinctBy(Resource::getFileName))
            .collect(Collectors.toCollection(ArrayList::new));

        boolean sortOrderAscending = true;
        String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size)

        // check for query
        if (query != null)
        {
            Fields params = new Fields();
            UrlEncoded.decodeUtf8To(query, 0, query.length(), params);

            String paramO = params.getValue("O");
            String paramC = params.getValue("C");
            if (StringUtil.isNotBlank(paramO))
            {
                switch (paramO)
                {
                    case "A" -> sortOrderAscending = true;
                    case "D" -> sortOrderAscending = false;
                }
            }
            if (StringUtil.isNotBlank(paramC))
            {
                if (paramC.equals("N") || paramC.equals("M") || paramC.equals("S"))
                {
                    sortColumn = paramC;
                }
            }
        }

        // Perform sort
        Comparator sort = switch (sortColumn)
        {
            case "M" -> ResourceCollators.byLastModified(sortOrderAscending);
            case "S" -> ResourceCollators.bySize(sortOrderAscending);
            default -> ResourceCollators.byFileName(sortOrderAscending);
        };
        listing.sort(sort);

        String decodedBase = URIUtil.decodePath(base);
        String title = "Directory: " + deTag(decodedBase);

        StringBuilder buf = new StringBuilder(4096);

        // Doctype Declaration + XHTML. The spec says the encoding MUST be "utf-8" in all cases at it is ignored;
        // see: https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset
        buf.append("""
            
            
            
            """);

        // HTML Header
        buf.append("\n");
        buf.append("\n");
        buf.append("");
        buf.append(title);
        buf.append("\n");
        buf.append("\n");

        // HTML Body
        buf.append("\n");
        buf.append("

").append(title).append("

\n"); // HTML Table final String ARROW_DOWN = "  ⇩"; final String ARROW_UP = "  ⇧"; buf.append("\n"); buf.append("\n"); String arrow = ""; String order = "A"; if (sortColumn.equals("N")) { if (sortOrderAscending) { order = "D"; arrow = ARROW_UP; } else { order = "A"; arrow = ARROW_DOWN; } } buf.append(""); arrow = ""; order = "A"; if (sortColumn.equals("M")) { if (sortOrderAscending) { order = "D"; arrow = ARROW_UP; } else { order = "A"; arrow = ARROW_DOWN; } } buf.append(""); arrow = ""; order = "A"; if (sortColumn.equals("S")) { if (sortOrderAscending) { order = "D"; arrow = ARROW_UP; } else { order = "A"; arrow = ARROW_DOWN; } } buf.append("\n"); buf.append("\n"); buf.append("\n"); String encodedBase = hrefEncodeURI(base); if (parent) { // Name buf.append(""); // Last Modified buf.append(""); // Size buf.append(""); buf.append("\n"); } // TODO: Use Locale and/or ZoneId from Request? DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM) .withZone(ZoneId.systemDefault()); for (Resource item : listing) { // Listings always return non-composite Resource entries String name = item.getFileName(); if (StringUtil.isBlank(name)) continue; // a resource either not backed by a filename (eg: MemoryResource), or has no filename (eg: a segment-less root "/") // Ensure name has a slash if it's a directory if (item.isDirectory() && !name.endsWith("/")) name += "/"; // Name buf.append(""); // Last Modified buf.append(""); // Size buf.append("\n"); } buf.append("\n"); buf.append("
"); buf.append("Name").append(arrow); buf.append(""); buf.append("Last Modified").append(arrow); buf.append(""); buf.append("Size").append(arrow); buf.append("
path, investigate if we can use relative links reliably now buf.append(URIUtil.addPaths(encodedBase, "../")); buf.append("\">Parent Directory--
"); buf.append(deTag(name)); buf.append(" "); Instant lastModified = item.lastModified(); buf.append(formatter.format(lastModified)); buf.append(" "); long length = item.length(); if (length >= 0) { buf.append(String.format("%,d bytes", item.length())); } buf.append(" 
\n"); buf.append("\n"); return buf.toString(); } /* TODO: see if we can use {@link Collectors#groupingBy} */ private static Predicate distinctBy(Function keyExtractor) { HashSet map = new HashSet<>(); return t -> map.add(keyExtractor.apply(t)); } /** *

* Encode any characters that could break the URI string in an HREF. *

* *

* Such as: * {@code Link} *

*

* The above example would parse incorrectly on various browsers as the "<" or '"' characters * would end the href attribute value string prematurely. *

* * @param raw the raw text to encode. * @return the defanged text. */ private static String hrefEncodeURI(String raw) { StringBuffer buf = null; loop: for (int i = 0; i < raw.length(); i++) { char c = raw.charAt(i); switch (c) { case '\'': case '"': case '<': case '>': buf = new StringBuffer(raw.length() << 1); break loop; default: break; } } if (buf == null) return raw; for (int i = 0; i < raw.length(); i++) { char c = raw.charAt(i); switch (c) { case '"' -> buf.append("%22"); case '\'' -> buf.append("%27"); case '<' -> buf.append("%3C"); case '>' -> buf.append("%3E"); default -> buf.append(c); } } return buf.toString(); } private static String deTag(String raw) { return StringUtil.sanitizeXmlString(raw); } }