/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.clients.http; import freenet.client.HighLevelSimpleClient; import freenet.l10n.NodeL10n; import freenet.node.NodeClientCore; import freenet.support.HTMLNode; import freenet.support.api.HTTPRequest; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Comparator; import java.util.Hashtable; import java.util.Map; /** * @author David 'Bombe' Roden <bombe@freenetproject.org> * @version $Id$ */ public abstract class LocalFileBrowserToadlet extends Toadlet { protected final NodeClientCore core; private static final int MAX_POST_SIZE = 1024*1024; public LocalFileBrowserToadlet (NodeClientCore core, HighLevelSimpleClient highLevelSimpleClient) { super(highLevelSimpleClient); this.core = core; } @Override public abstract String path(); protected abstract String postTo(); /** * Last directory from which an action was performed. If accessible, this is used instead of the fallback * default directory. */ private File lastSuccessful; /** * Part set when a directory is selected. */ public static final String selectDir = "select-dir"; /** * Part set when a file is selected. */ public static final String selectFile = "select-file"; /** * Part set when a directory is changed. */ public static final String changeDir = "change-dir"; /** * @return Part which contains selected directory or file. */ protected String filenameField() { return "filename"; } /** * Whether the directory is allowed for the purposes of the specific browser. For example, do the node settings * allow downloading to the given directory? * @param path The path to check permissions for. * @return Whether browsing that directory is allowed. If it is not, the LocalFileBrowserToadlet will not render * a selection button or directory-changing button for it. */ protected abstract boolean allowedDir(File path); /** * Performs sanity checks and generates parameter persistence fields. * @param set page parts/parameters * @return fieldPairs correct fields to persist for this browser type */ protected abstract Hashtable<String, String> persistenceFields (Hashtable<String, String> set); /** * Determines the appropriate directory to start out in for the given browser. If a path is not already * specified, the browser will attempt to display this directory. * @return path to directory */ protected abstract String startingDir(); /** * Determines the appropriate directory to start out in if browsing for something to upload. * @return If all directories or no directories are allowed, returns the user's home directory. * Otherwise, returns the first allowed directory. */ protected String defaultUploadDir() { if ((core.getAllowedUploadDirs().length == 1 && core.getAllowedUploadDirs()[0].toString().equals("all")) || core.getAllowedUploadDirs().length == 0) { /* If all directories are allowed, or none are, go for the home directory. * If none are allowed, any directory will result in an error anyway. */ return System.getProperty("user.home"); } //If locations are explicitly specified take the first one. return core.getAllowedUploadDirs()[0].getAbsolutePath(); } /** * Determines the appropriate directory to start out in if browsing for something to download. * @return If all directories or no directories are allowed, the default downloads directory is returned. * Otherwise, returns the first allowed directory. */ protected String defaultDownloadDir() { if ((core.getAllowedDownloadDirs().length == 1 && core.getAllowedDownloadDirs()[0].toString().equals("all")) || core.getAllowedDownloadDirs().length == 0) { /* If all directories are allowed, or none are, go for the default download directory. * If none are allowed, any directory will result in an error anyway. */ return core.getDownloadsDir().getAbsolutePath(); } //If locations are explicitly specified take the first one. return core.getAllowedDownloadDirs()[0].getAbsolutePath(); } /** * Renders directory selection button with selectDir and filenameField() set. * @param node Node to add the button to. * @param absolutePath Path to set the filenameField() field to. * @param persistence Additional persistence fields to include. */ protected void createSelectDirectoryButton (HTMLNode node, String absolutePath, HTMLNode persistence) { node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"submit", selectDir, l10n("insert")}); node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"hidden", filenameField(), absolutePath}); node.addChild(persistence); } /** * Renders file selection button with selectFile and filenameField() set. * @param node Node to add the button to. * @param absolutePath Path to set the filenameField() field to. * @param persistence Additional persistence fields to include. */ protected void createSelectFileButton (HTMLNode node, String absolutePath, HTMLNode persistence) { node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"submit", selectFile, l10n("insert")}); node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"hidden", filenameField(), absolutePath}); node.addChild(persistence); } /** * Renders directory changing button with changeDir and filenameField() set. * @param node Node to add the button to. * @param path Path to set the "path" field to. * @param persistence Additional persistence fields to include. */ private void createChangeDirButton (HTMLNode node, String buttonText, String path, HTMLNode persistence) { node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"submit", changeDir, buttonText}); node.addChild("input", new String[]{"type", "name", "value"}, new String[]{"hidden", "path", path}); node.addChild(persistence); } /** * Returns a Hashtable of all URL parameters. * @param request contains URL parameters * @return Hashtable of all GET params. */ private Hashtable<String, String> readGET (HTTPRequest request) { Hashtable<String, String> set = new Hashtable<String, String>(); for (String key : request.getParameterNames()) { set.put(key, request.getParam(key)); } return set; } /** * Returns a Hashtable of all POST parts up to a length of 1024*1024 characters. * @param request contains POST parts * @return set a Hashtable of all POST parts. */ private Hashtable<String, String> readPOST (HTTPRequest request) { Hashtable<String, String> set = new Hashtable<String, String>(); for (String key : request.getParts()) { set.put(key, request.getPartAsStringFailsafe(key, MAX_POST_SIZE)); } return set; } /** * Renders hidden fields. * @param fieldPairs Pairs of values to be rendered * @return result HTMLNode containing hidden persistence fields */ private HTMLNode renderPersistenceFields (Hashtable<String, String> fieldPairs) { HTMLNode result = new HTMLNode("div", "id", "persistenceFields"); for (Map.Entry<String,String> entry : fieldPairs.entrySet()) { result.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", entry.getKey(), entry.getValue() }); } return result; } private String selectedValue(HTTPRequest request) { if (request.isParameterSet(filenameField()) && (request.isParameterSet(selectDir) || request.isParameterSet(selectFile))) { //Request is a GET. return request.getParam(filenameField()); } else if (request.isPartSet(filenameField()) && (request.isPartSet(selectDir) || request.isPartSet(selectFile))) { //Request is a POST. return request.getPartAsStringFailsafe(filenameField(), MAX_POST_SIZE); } return null; } /** * @param uri is unused, * @param request contains parameters. * @param ctx allows page rendering and permissions checks. * @exception ToadletContextClosedException Access is denied: uploading might be disabled overall. * The user might be denied access to this directory, * which could be their home directory. * @exception IOException Something file-related went wrong. * @see <a href="freenet/clients/http/Toadlet#findSupportedMethods()">findSupportedMethods</a> * @see "java.net.URI" * @see "<a href="freenet/clients/http/ToadletContext.html">ToadletContext</a> */ public void handleMethodGET (URI uri, HTTPRequest request, final ToadletContext ctx) throws ToadletContextClosedException, IOException, RedirectException { renderPage(persistenceFields(readGET(request)), request.getParam("path"), ctx, selectedValue(request)); } public void handleMethodPOST (URI uri, HTTPRequest request, final ToadletContext ctx) throws ToadletContextClosedException, IOException, RedirectException { renderPage(persistenceFields(readPOST(request)), request.getPartAsStringFailsafe("path", MAX_POST_SIZE), ctx, selectedValue(request)); } /** * Presents a file selection screen, or a something has been selected notes its directory and redirects to the * POST target. * @param fieldPairs fields which are to be persisted between views * @param path current path to display * @param ctx context used for rendering * @param filename a filename if a file or directory is selected, NULL if not. * @throws ToadletContextClosedException * @throws IOException * @throws RedirectException */ private void renderPage (Hashtable<String, String> fieldPairs, String path, final ToadletContext ctx, String filename) throws ToadletContextClosedException, IOException, RedirectException { HTMLNode persistenceFields = renderPersistenceFields(fieldPairs); if (filename != null) { File file = new File(filename); if (file.isDirectory()) lastSuccessful = file.getAbsoluteFile(); else lastSuccessful = file.getParentFile().getAbsoluteFile(); try { throw new RedirectException(postTo()); } catch (URISyntaxException e) { sendErrorPage(ctx, 500, NodeL10n.getBase().getString("Toadlet.internalErrorPleaseReport"), e.getMessage()); } } if (path.length() == 0) { if (lastSuccessful != null && lastSuccessful.isDirectory() && allowedDir(lastSuccessful)) { path = lastSuccessful.getAbsolutePath(); } else { path = startingDir(); } } File currentPath = new File(path).getCanonicalFile(); //For use in error messages. String attemptedPath = currentPath == null ? "null" : currentPath.getAbsolutePath(); PageMaker pageMaker = ctx.getPageMaker(); if (currentPath != null && !allowedDir(currentPath)) { PageNode page = pageMaker.getPageNode(l10n("listingTitle", "path", attemptedPath), ctx); pageMaker.getInfobox("infobox-error", "Forbidden", page.content, "access-denied", true). addChild("#", l10n("dirAccessDenied")); sendErrorPage(ctx, 403, "Forbidden", l10n("dirAccessDenied")); return; } HTMLNode pageNode; if (currentPath != null && currentPath.exists() && currentPath.isDirectory() && currentPath.canRead()) { PageNode page = pageMaker.getPageNode(l10n("listingTitle", "path", currentPath.getAbsolutePath()), ctx); pageNode = page.outer; HTMLNode contentNode = page.content; if (ctx.isAllowedFullAccess()) contentNode.addChild(ctx.getAlertManager().createSummary()); HTMLNode infoboxDiv = contentNode.addChild("div", "class", "infobox"); infoboxDiv.addChild("div", "class", "infobox-header", l10n("listing", "path", currentPath.getAbsolutePath())); HTMLNode listingDiv = infoboxDiv.addChild("div", "class", "infobox-content"); File[] files = currentPath.listFiles(); if (files == null) { sendErrorPage(ctx, 403, "Forbidden", l10n("dirAccessDenied")); return; } Arrays.sort(files, new Comparator<File>() { @Override public int compare(File firstFile, File secondFile) { /* Put directories above files, sorting each alphabetically and * case-insensitively. */ if (firstFile.isDirectory() && !secondFile.isDirectory()) { return -1; } if (!firstFile.isDirectory() && secondFile.isDirectory()) { return 1; } return firstFile.getName().compareToIgnoreCase(secondFile.getName()); } }); HTMLNode listingTable = listingDiv.addChild("table"); HTMLNode headerRow = listingTable.addChild("tr"); headerRow.addChild("th"); headerRow.addChild("th", l10n("fileHeader")); headerRow.addChild("th", l10n("sizeHeader")); /* add filesystem roots (fsck windows) */ for (File currentRoot : File.listRoots()) { if (allowedDir(currentRoot)) { HTMLNode rootRow = listingTable.addChild("tr"); rootRow.addChild("td"); HTMLNode rootLinkCellNode = rootRow.addChild("td"); HTMLNode rootLinkFormNode = ctx.addFormChild(rootLinkCellNode, path(), "insertLocalFileForm"); createChangeDirButton(rootLinkFormNode, currentRoot.getCanonicalPath(), currentRoot.getAbsolutePath(), persistenceFields); rootRow.addChild("td"); } } /* add back link */ if (currentPath.getParent() != null) { if (allowedDir(currentPath.getParentFile())) { HTMLNode backlinkRow = listingTable.addChild("tr"); backlinkRow.addChild("td"); HTMLNode backLinkCellNode = backlinkRow.addChild("td"); HTMLNode backLinkFormNode = ctx.addFormChild(backLinkCellNode, path(), "insertLocalFileForm"); createChangeDirButton(backLinkFormNode, "..", currentPath.getParent(), persistenceFields); backlinkRow.addChild("td"); } } for (File currentFile : files) { if(currentFile.isHidden()) { // It won't be inserted if we insert the whole folder. // So lets just ignore it, less confusing. // FIXME in advanced mode, show them, and have a different style, and a toggle for whether to include them in a folder insert. continue; } HTMLNode fileRow = listingTable.addChild("tr"); if (currentFile.isDirectory()) { if (currentFile.canRead()) { // Select directory if (allowedDir(currentFile)) { HTMLNode cellNode = fileRow.addChild("td"); HTMLNode formNode = ctx.addFormChild(cellNode, path(), "insertLocalFileForm"); createSelectDirectoryButton(formNode, currentFile.getAbsolutePath(), persistenceFields); // Change directory HTMLNode directoryCellNode = fileRow.addChild("td"); HTMLNode directoryFormNode = ctx.addFormChild(directoryCellNode, path(), "insertLocalFileForm"); createChangeDirButton(directoryFormNode, currentFile.getName(), currentFile.getAbsolutePath(), persistenceFields); } } else { fileRow.addChild("td"); fileRow.addChild("td", "class", "unreadable-file", currentFile.getName()); } fileRow.addChild("td"); } else { if (currentFile.canRead()) { //Select file HTMLNode cellNode = fileRow.addChild("td"); HTMLNode formNode = ctx.addFormChild(cellNode, path(), "insertLocalFileForm"); createSelectFileButton(formNode, currentFile.getAbsolutePath(), persistenceFields); fileRow.addChild("td", currentFile.getName()); fileRow.addChild("td", "class", "right-align", String.valueOf(currentFile.length())); } else { fileRow.addChild("td"); fileRow.addChild("td", "class", "unreadable-file", currentFile.getName()); fileRow.addChild("td", "class", "right-align", String.valueOf(currentFile.length())); } } } } else { PageNode page = pageMaker.getPageNode(l10n("listingTitle", "path", attemptedPath), ctx); pageNode = page.outer; HTMLNode contentNode = page.content; if (ctx.isAllowedFullAccess()) contentNode.addChild(ctx.getAlertManager().createSummary()); HTMLNode infoboxDiv = contentNode.addChild("div", "class", "infobox"); infoboxDiv.addChild("div", "class", "infobox-header", l10n("listing", "path", attemptedPath)); HTMLNode listingDiv = infoboxDiv.addChild("div", "class", "infobox-content"); listingDiv.addChild("#", l10n("dirCannotBeRead", "path", attemptedPath)); HTMLNode ulNode = listingDiv.addChild("ul"); ulNode.addChild("li", l10n("checkPathExist")); ulNode.addChild("li", l10n("checkPathIsDir")); ulNode.addChild("li", l10n("checkPathReadable")); } writeHTMLReply(ctx, 200, "OK", pageNode.generate()); } private String l10n (String key, String pattern, String value) { return NodeL10n.getBase().getString("LocalFileInsertToadlet."+key, new String[] { pattern }, new String[] { value }); } private String l10n(String msg) { return NodeL10n.getBase().getString("LocalFileInsertToadlet."+msg); } }