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}