001package jmri.web.servlet.directory;
002
003import java.io.IOException;
004import java.text.DateFormat;
005import java.util.Arrays;
006import java.util.Date;
007import java.util.Locale;
008import jmri.InstanceManager;
009import jmri.util.FileUtil;
010import jmri.web.servlet.ServletUtil;
011import org.eclipse.jetty.util.StringUtil;
012import org.eclipse.jetty.util.URIUtil;
013import org.eclipse.jetty.util.resource.PathResource;
014import org.eclipse.jetty.util.resource.Resource;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Override
020 * {@link org.eclipse.jetty.util.resource.Resource#getListHTML(java.lang.String, boolean, java.lang.String)}
021 * in {@link org.eclipse.jetty.util.resource.Resource} so that directory
022 * listings can include the complete JMRI look and feel.
023 *
024 * @author Randall Wood Copright 2016, 2020
025 */
026public class DirectoryResource extends PathResource {
027
028    private final Locale locale;
029
030    public DirectoryResource(Locale locale, Resource resource) throws IOException {
031        super(resource.getFile());
032        this.locale = locale;
033    }
034
035    @Override
036    public String getListHTML(String base, boolean parent, String query)
037            throws IOException {
038        String basePath = URIUtil.canonicalPath(base);
039        if (basePath == null || !isDirectory()) {
040            return null;
041        }
042
043        String[] ls = list();
044        if (ls == null) {
045            return null;
046        }
047        Arrays.sort(ls);
048
049        String decodedBase = URIUtil.decodePath(basePath);
050        String title = Bundle.getMessage(this.locale, "DirectoryTitle", StringUtil.sanitizeXmlString(decodedBase)); // NOI18N
051
052        StringBuilder table = new StringBuilder(4096);
053        String row = Bundle.getMessage(this.locale, "TableRow"); // NOI18N
054        if (parent) {
055            table.append(String.format(this.locale, row,
056                    URIUtil.addPaths(basePath, "../"),
057                    Bundle.getMessage(this.locale, "ParentDirectory"),
058                    "",
059                    ""));
060        }
061
062        String encodedBase = hrefEncodeURI(basePath);
063
064        DateFormat dfmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, this.locale);
065        for (String l : ls) {
066            Resource item = addPath(l);
067            String itemPath = URIUtil.addPaths(encodedBase, URIUtil.encodePath(l));
068            if (item.isDirectory() && !itemPath.endsWith("/")) {
069                itemPath += URIUtil.SLASH;
070            }
071            table.append(String.format(this.locale, row,
072                    itemPath,
073                    StringUtil.sanitizeXmlString(l),
074                    Bundle.getMessage(this.locale, "SizeInBytes", item.length()),
075                    dfmt.format(new Date(item.lastModified())))
076            );
077        }
078
079        return String.format(this.locale,
080                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(this.locale, "Directory.html"))), // NOI18N
081                String.format(this.locale,
082                        Bundle.getMessage(this.locale, "HtmlTitle"), // NOI18N
083                        InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
084                        title
085                ),
086                InstanceManager.getDefault(ServletUtil.class).getNavBar(this.locale, basePath),
087                InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
088                InstanceManager.getDefault(ServletUtil.class).getFooter(this.locale, basePath),
089                title,
090                table
091        );
092    }
093
094    @Override
095    public boolean equals(Object other) {
096        // spotbugs errors if equals is not overridden, so override and call super
097        return super.equals(other);
098    }
099
100    @Override
101    public int hashCode() {
102        // spotbugs errors if equals is present, but not hashCode, so override and call super
103        return super.hashCode();
104    }
105
106    /*
107     * Originally copied from private static method of org.eclipse.jetty.util.resource.Resource
108     */
109    /**
110     * Encode any characters that could break the URI string in an HREF. Such as
111     * <a
112     * href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
113     *
114     * The above example would parse incorrectly on various browsers as the "<"
115     * or '"' characters would end the href attribute value string prematurely.
116     *
117     * @param raw the raw text to encode.
118     * @return the defanged text.
119     */
120    private static String hrefEncodeURI(String raw) {
121        StringBuilder buf = null;
122
123        loop:
124        for (int i = 0; i < raw.length(); i++) {
125            char c = raw.charAt(i);
126            switch (c) {
127                case '\'':
128                case '"':
129                case '<':
130                case '>':
131                    buf = new StringBuilder(raw.length() << 1);
132                    break loop;
133                default:
134                    log.debug("Unhandled code: {}", c);
135                    break;
136            }
137        }
138        if (buf == null) {
139            return raw;
140        }
141
142        for (int i = 0; i < raw.length(); i++) {
143            char c = raw.charAt(i);
144            switch (c) {
145                case '"':
146                    buf.append("%22");
147                    break;
148                case '\'':
149                    buf.append("%27");
150                    break;
151                case '<':
152                    buf.append("%3C");
153                    break;
154                case '>':
155                    buf.append("%3E");
156                    break;
157                default:
158                    buf.append(c);
159            }
160        }
161
162        return buf.toString();
163    }
164
165    // initialize logging
166    private static final Logger log = LoggerFactory.getLogger(DirectoryResource.class);
167}