/* 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 java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

import freenet.client.DefaultMIMETypes;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.HighLevelSimpleClient;
import freenet.client.HighLevelSimpleClientImpl;
import freenet.client.InsertContext;
import freenet.client.InsertContext.CompatibilityMode;
import freenet.client.MetadataUnresolvedException;
import freenet.client.async.ClientContext;
import freenet.client.async.PersistenceDisabledException;
import freenet.client.async.PersistentJob;
import freenet.client.async.TooManyFilesInsertException;
import freenet.client.filter.ContentFilter;
import freenet.client.filter.FilterMIMEType;
import freenet.client.filter.KnownUnsafeContentTypeException;
import freenet.clients.fcp.ClientGet;
import freenet.clients.fcp.ClientPut;
import freenet.clients.fcp.ClientPut.COMPRESS_STATE;
import freenet.clients.fcp.ClientPutBase.UploadFrom;
import freenet.clients.fcp.ClientPutDir;
import freenet.clients.fcp.ClientRequest;
import freenet.clients.fcp.ClientRequest.Persistence;
import freenet.clients.fcp.DownloadRequestStatus;
import freenet.clients.fcp.FCPServer;
import freenet.clients.fcp.IdentifierCollisionException;
import freenet.clients.fcp.MessageInvalidException;
import freenet.clients.fcp.NotAllowedException;
import freenet.clients.fcp.RequestCompletionCallback;
import freenet.clients.fcp.RequestStatus;
import freenet.clients.fcp.UploadDirRequestStatus;
import freenet.clients.fcp.UploadFileRequestStatus;
import freenet.clients.fcp.UploadRequestStatus;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.DarknetPeerNode;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.RequestStarter;
import freenet.node.SecurityLevels.PHYSICAL_THREAT_LEVEL;
import freenet.node.useralerts.StoringUserEvent;
import freenet.node.useralerts.UserAlert;
import freenet.support.Fields;
import freenet.support.HTMLNode;
import freenet.support.HexUtil;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.MultiValueTable;
import freenet.support.SizeUtil;
import freenet.support.TimeUtil;
import freenet.support.api.HTTPRequest;
import freenet.support.api.HTTPUploadedFile;
import freenet.support.api.RandomAccessBucket;
import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.NativeThread;

public class QueueToadlet extends Toadlet implements RequestCompletionCallback, LinkEnabledCallback {

	public enum QueueColumn {
		IDENTIFIER,
		SIZE,
		MIME_TYPE,
		PERSISTENCE,
		KEY,
		FILENAME,
		PRIORITY,
		FILES,
		TOTAL_SIZE,
		PROGRESS,
		REASON,
		LAST_ACTIVITY,
		LAST_FAILURE,
		COMPAT_MODE
	}

	private enum QueueType {
		CompletedDownloadToTemp(true, false, false),
		CompletedDownloadToDisk(true, false, false),
		CompletedUpload(true, false, true),
		CompletedDirUpload(true, false, true),
		FailedDownload(false, true, false),
		FailedUpload(false, true, true),
		FailedDirUpload(false, true, true),
		FailedBadMIMEType(false, true, false),
		FailedUnknownMIMEType(false, true, false),
		UncompletedDownload(false, false, false),
		UncompletedUpload(false, false, true),
		UncompletedDirUpload(false, false, true);

		final boolean isCompleted;
		final boolean isFailed;
		final boolean isUpload;

		private QueueType(boolean isCompleted, boolean isFailed, boolean isUpload) {
			this.isCompleted = isCompleted;
			this.isFailed = isFailed;
			this.isUpload = isUpload;
		}
	}

	private static final int MAX_IDENTIFIER_LENGTH = 1024*1024;
	static final int MAX_FILENAME_LENGTH = 1024*1024;
	private static final int MAX_TYPE_LENGTH = 1024;
	static final int MAX_KEY_LENGTH = 1024*1024;

	private NodeClientCore core;
	final FCPServer fcp;
	private FileInsertWizardToadlet fiw;

	private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	void setFIW(FileInsertWizardToadlet fiw) {
		this.fiw = fiw;
	}

	private boolean isReversed = false;
	private final boolean uploads;

    private static final String KEY_LIST_LOCATION = "listKeys.txt";

	public QueueToadlet(NodeClientCore core, FCPServer fcp, HighLevelSimpleClient client, boolean uploads) {
		super(client);
		this.core = core;
		this.fcp = fcp;
		this.uploads = uploads;
		if(fcp == null) throw new NullPointerException();
		fcp.setCompletionCallback(this);
		try {
			loadCompletedIdentifiers();
		} catch (PersistenceDisabledException e) {
			// The user will know soon enoughUpdate Toadlet.java
		}
	}
	
	public void handleMethodPOST(URI uri, HTTPRequest request, final ToadletContext ctx) throws ToadletContextClosedException, IOException, RedirectException {

		if(container.publicGatewayMode() && !ctx.isAllowedFullAccess()) {
		    sendUnauthorizedPage(ctx);
			return;
		}

		try {
			// Browse... button on upload page
			if (request.isPartSet("insert-local")) {
				
				FreenetURI insertURI;
				String keyType = request.getPartAsStringFailsafe("keytype", 10);
				if ("CHK".equals(keyType)) {
					insertURI = new FreenetURI("CHK@");
					if(fiw != null)
						fiw.reportCanonicalInsert();
				} else if("SSK".equals(keyType)) {
					insertURI = new FreenetURI("SSK@");
					if(fiw != null)
						fiw.reportRandomInsert();
				} else if("specify".equals(keyType)) {
					try {
						String u = request.getPartAsStringFailsafe("key", MAX_KEY_LENGTH);
						insertURI = new FreenetURI(u);
						if(logMINOR)
							Logger.minor(this, "Inserting key: "+insertURI+" ("+u+")");
					} catch (MalformedURLException mue1) {
						writeError(l10n("errorInvalidURI"),
							   l10n("errorInvalidURIToU"), ctx, false, true);
						return;
					}
				} else {
					writeError(l10n("errorMustSpecifyKeyTypeTitle"),
						   l10n("errorMustSpecifyKeyType"), ctx, false, true);
					return;
				}
				MultiValueTable<String, String> responseHeaders = MultiValueTable.from(
					"Location",
					LocalFileInsertToadlet.PATH
						+ "?key=" + insertURI.toASCIIString()
						+ "&compress=" + (request.getPartAsStringFailsafe("compress", 128).isEmpty())
						+ "&compatibilityMode=" + request.getPartAsStringFailsafe("compatibilityMode", 100)
						+ "&overrideSplitfileKey=" + request.getPartAsStringFailsafe("overrideSplitfileKey", 65)
				);
				ctx.sendReplyHeaders(302, "Found", responseHeaders, null, 0);
				return;
			} else if (request.isPartSet("select-location")) {
				try {
					throw new RedirectException(LocalDirectoryConfigToadlet.basePath()+"/downloads/");
				} catch (URISyntaxException e) {
					//Shouldn't happen, path is defined as such.
				}
			}

			if(request.isPartSet("delete_request") && !request.getPartAsStringFailsafe("delete_request", 128).isEmpty()) {
				// Confirm box
				PageNode page = ctx.getPageMaker().getPageNode(l10n("confirmDeleteTitle"), ctx);
				HTMLNode inner = page.content;
				HTMLNode content = ctx.getPageMaker().getInfobox("infobox-warning", l10n("confirmDeleteTitle"), inner, "confirm-delete-title", true);
				
				HTMLNode deleteNode = new HTMLNode("p");
				HTMLNode deleteForm = ctx.addFormChild(deleteNode, path(), "queueDeleteForm");
				HTMLNode infoList = deleteForm.addChild("ul");
				
				for(String part : request.getParts()) {
					if(!part.startsWith("identifier-")) continue;
					part = part.substring("identifier-".length());
					if(part.length() > 50) continue; // It's just a number 
					
					String identifier = request.getPartAsStringFailsafe("identifier-"+part, MAX_IDENTIFIER_LENGTH);
					if(identifier == null) continue;
					String filename = request.getPartAsStringFailsafe("filename-"+part, MAX_FILENAME_LENGTH);
					String keyString = request.getPartAsStringFailsafe("key-"+part, MAX_KEY_LENGTH);
					String type = request.getPartAsStringFailsafe("type-"+part, MAX_TYPE_LENGTH);
					String size = request.getPartAsStringFailsafe("size-"+part, 50);
					if(filename != null) {
						HTMLNode line = infoList.addChild("li");
						line.addChild("#", NodeL10n.getBase().getString("FProxyToadlet.filenameLabel")+" ");
						if(keyString != null) {
							line.addChild("a", "href", "/"+keyString, filename);
						} else {
							line.addChild("#", filename);
						}
					}
					if(type != null && !type.isEmpty()) {
						HTMLNode line = infoList.addChild("li");
						boolean finalized = request.isPartSet("finalizedType");
						line.addChild("#", NodeL10n.getBase().getString("FProxyToadlet."+(finalized ? "mimeType" : "expectedMimeType"), new String[] { "mime" }, new String[] { type }));
					}
					if(size != null) {
						HTMLNode line = infoList.addChild("li");
						line.addChild("#", NodeL10n.getBase().getString("FProxyToadlet.sizeLabel") + " " + size);
					}
					infoList.addChild("#", l10n("deleteFileFromTemp"));
					infoList.addChild("input", new String[] { "type", "name", "value", "checked" },
						new String[] { "checkbox", "identifier-"+part, identifier, "checked" });
				}
				
				content.addChild("p", l10n("confirmDelete"));
				content.addChild(deleteNode);

				deleteForm.addChild("input",
					new String[] { "type", "name", "value" },
					new String[] { "submit", "remove_request", NodeL10n.getBase().getString("Toadlet.yes") });
				deleteForm.addChild("input",
					new String[] { "type", "name", "value" },
					new String[] { "submit", "cancel", NodeL10n.getBase().getString("Toadlet.no") });

				this.writeHTMLReply(ctx, 200, "OK", page.outer.generate());
				return;
			} else if(request.isPartSet("remove_request") && !request.getPartAsStringFailsafe("remove_request", 128).isEmpty()) {
				// Remove all requested (i.e. selected) requests from the queue, regardless of
				// their status
				// FIXME optimise into a single database job.
				
				String identifier = "";
				try {
					for(String part : request.getParts()) {
						if(!part.startsWith("identifier-")) continue;
						identifier = part.substring("identifier-".length());
						if(identifier.length() > 50) continue;
						identifier = request.getPartAsStringFailsafe(part, MAX_IDENTIFIER_LENGTH);
						if(logMINOR) Logger.minor(this, "Removing "+identifier);
						fcp.removeGlobalRequestBlocking(identifier);
					}
				} catch (MessageInvalidException e) {
					this.sendErrorPage(ctx, 200,
							l10n("failedToRemoveRequest"),
							l10n("failedToRemove",
								new String[]{ "id", "message" },
								new String[]{ identifier, e.getMessage()}
							));
					return;
				} catch (PersistenceDisabledException e) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
			} else if(request.isPartSet("remove_finished_uploads_request") && !request.getPartAsStringFailsafe("remove_finished_uploads_request", 128).isEmpty()) {
				// Remove all finished single-file uploads
				String identifier = "";
				try {
					RequestStatus[] reqs = fcp.getGlobalRequests();
					for (RequestStatus r : reqs) {
						if (r instanceof UploadFileRequestStatus) {
							UploadFileRequestStatus upload = (UploadFileRequestStatus) r;
							if (upload.hasSucceeded()) {
								identifier = upload.getIdentifier();
								fcp.removeGlobalRequestBlocking(identifier);
							}
						}
					}
				} catch (MessageInvalidException e) {
					this.sendErrorPage(ctx, 200,
							l10n("failedToRemoveRequest"),
							l10n("failedToRemove",
								new String[]{ "id", "message" },
								new String[]{ identifier, e.getMessage()}
							));
					return;
				} catch (PersistenceDisabledException e) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
				
			} else if(request.isPartSet("remove_finished_downloads_request") && !request.getPartAsStringFailsafe("remove_finished_downloads_request", 128).isEmpty()) {
				// Remove all finished downloads
				String identifier = "";
				try {
					RequestStatus[] reqs = fcp.getGlobalRequests();
					for (RequestStatus r : reqs) {
						if (r instanceof DownloadRequestStatus) {
							DownloadRequestStatus download = (DownloadRequestStatus) r;
							if (download.isPersistent() && download.hasSucceeded() && download.isTotalFinalized() && !download.toTempSpace()) {
								identifier = download.getIdentifier();
								fcp.removeGlobalRequestBlocking(identifier);
							}
						}
					}
				} catch (MessageInvalidException e) {
					this.sendErrorPage(ctx, 200,
							l10n("failedToRemoveRequest"),
							l10n("failedToRemove",
								new String[]{ "id", "message" },
								new String[]{ identifier, e.getMessage()}
							));
					return;
				} catch (PersistenceDisabledException e) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
			}
			else if(request.isPartSet("restart_request") && !request.getPartAsStringFailsafe("restart_request", 128).isEmpty()) {
				boolean disableFilterData = request.isPartSet("disableFilterData");
				
				
				String identifier = "";
				for(String part : request.getParts()) {
					if(!part.startsWith("identifier-")) continue;
					identifier = part.substring("identifier-".length());
					if(identifier.length() > 50) continue;
					identifier = request.getPartAsStringFailsafe(part, MAX_IDENTIFIER_LENGTH);
					if(logMINOR) Logger.minor(this, "Restarting "+identifier);
					try {
						fcp.restartBlocking(identifier, disableFilterData);
					} catch (PersistenceDisabledException e) {
						sendPersistenceDisabledError(ctx);
						return;
					}
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
			} else if(request.isPartSet("panic") && !request.getPartAsStringFailsafe("panic", 128).isEmpty()) {
				if(SimpleToadletServer.noConfirmPanic) {
					core.getNode().killMasterKeysFile();
					core.getNode().panic();
					sendPanicingPage(ctx);
					core.getNode().finishPanic();
					return;
				} else {
					sendConfirmPanicPage(ctx);
					return;
				}
			} else if(request.isPartSet("confirmpanic") && !request.getPartAsStringFailsafe("confirmpanic", 128).isEmpty()) {
				core.getNode().killMasterKeysFile();
				core.getNode().panic();
				sendPanicingPage(ctx);
				core.getNode().finishPanic();
				return;
			} else if(request.isPartSet("download")) {
				// Queue a download
				if(!request.isPartSet("key")) {
					writeError(l10n("errorNoKey"), l10n("errorNoKeyToD"), ctx);
					return;
				}
				String expectedMIMEType = null;
				if(request.isPartSet("type")) {
					expectedMIMEType = request.getPartAsStringFailsafe("type", MAX_TYPE_LENGTH);
				}
				FreenetURI fetchURI;
				try {
					fetchURI = new FreenetURI(request.getPartAsStringFailsafe("key", MAX_KEY_LENGTH));
				} catch (MalformedURLException e) {
					writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToD"), ctx);
					return;
				}
				String persistence = request.getPartAsStringFailsafe("persistence", 32);
				String returnType = request.getPartAsStringFailsafe("return-type", 32);
				boolean filterData = request.isPartSet("filterData");
				String downloadPath;
				File downloadsDir = null;
				//Download to disk disabled and initialized.
				if (request.isPartSet("path") && !core.isDownloadDisabled()) {
					downloadPath = request.getPartAsStringFailsafe("path", MAX_FILENAME_LENGTH);
					try {
						downloadsDir = getDownloadsDir(downloadPath);
					} catch (NotAllowedException e) {
						downloadDisallowedPage(e, downloadPath, ctx);
						return;
					}
				//Downloading to disk not initialized and/or disabled.
				} else returnType = "direct";
				try {
					fcp.makePersistentGlobalRequestBlocking(fetchURI, filterData, expectedMIMEType, persistence, returnType, false, downloadsDir);
				} catch (NotAllowedException e) {
					this.writeError(l10n("errorDToDisk"), l10n("errorDToDiskConfig"), ctx);
					return;
				} catch (PersistenceDisabledException e) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
			} else if(request.isPartSet("bulkDownloads")) {
				String bulkDownloadsAsString = request.getPartAsStringFailsafe("bulkDownloads", 262144);
				String[] keys = bulkDownloadsAsString.split("\n");
				if(bulkDownloadsAsString.isEmpty() || (keys.length < 1)) {
					writePermanentRedirect(ctx, "Done", path());
					return;
				}
				LinkedList<String> success = new LinkedList<String>(), failure = new LinkedList<String>();
				boolean filterData = request.isPartSet("filterData");
				String target = request.getPartAsStringFailsafe("target", 128);
				if(target == null) target = "direct";
				String downloadPath;
				File downloadsDir = null;
				if (request.isPartSet("path") && !core.isDownloadDisabled()) {
					downloadPath = request.getPartAsStringFailsafe("path", MAX_FILENAME_LENGTH);
					try {
						downloadsDir = getDownloadsDir(downloadPath);
					} catch (NotAllowedException e) {
						downloadDisallowedPage(e, downloadPath, ctx);
						return;
					}
				} else target = "direct";

				for(int i=0; i<keys.length; i++) {
					String currentKey = keys[i];

					// trim leading/trailing space
					currentKey = currentKey.trim();
					if (currentKey.isEmpty())
						continue;

					try {
						FreenetURI fetchURI = new FreenetURI(currentKey);
						fcp.makePersistentGlobalRequestBlocking(fetchURI, filterData, null,
							"forever", target, false, downloadsDir);
						success.add(fetchURI.toString(true, false));
					} catch (Exception e) {
						failure.add(currentKey);
						Logger.error(this,
							"An error occured while attempting to download key("+i+") : "+
							currentKey+ " : "+e.getMessage());
					}
				}

				boolean displayFailureBox = !failure.isEmpty();
				boolean displaySuccessBox = !success.isEmpty();

				PageNode page = ctx.getPageMaker().getPageNode(l10n("downloadFiles"), ctx);
				HTMLNode pageNode = page.outer;
				HTMLNode contentNode = page.content;

				HTMLNode alertContent = ctx.getPageMaker().getInfobox(
					(displayFailureBox ? "infobox-warning" : "infobox-info"),
					l10n("downloadFiles"), contentNode, "grouped-downloads", true);
				if(displaySuccessBox) {
					HTMLNode successDiv = alertContent.addChild("ul");
					successDiv.addChild("#", l10n("enqueuedSuccessfully", "number",
						String.valueOf(success.size())));
					for(String s: success) {
						HTMLNode line = successDiv.addChild("li");
						line.addChild("#", s);
					}
					successDiv.addChild("br");
				}
				if(displayFailureBox) {
					HTMLNode failureDiv = alertContent.addChild("ul");
					if(displayFailureBox) {
						failureDiv.addChild("#", l10n("enqueuedFailure", "number",
							String.valueOf(failure.size())));
						for(String f: failure) {
							HTMLNode line = failureDiv.addChild("li");
							line.addChild("#", f);
						}
					}
					failureDiv.addChild("br");
				}
				alertContent.addChild("a", "href", path(),
					NodeL10n.getBase().getString("Toadlet.returnToQueuepage"));
				writeHTMLReply(ctx, 200, "OK", pageNode.generate());
				return;
			} else if (request.isPartSet("change_priority_top")) {
				handleChangePriority(request, ctx, "_top");
				return;
			} else if (request.isPartSet("change_priority_bottom")) {
				handleChangePriority(request, ctx, "_bottom");
				return;
				// FIXME factor out the next 3 items, they are very messy!
			} else if (!request.getPartAsStringFailsafe("insert", 128).isEmpty()) {
				final FreenetURI insertURI;
				String keyType = request.getPartAsStringFailsafe("keytype", 10);
				if ("CHK".equals(keyType)) {
					insertURI = new FreenetURI("CHK@");
					if(fiw != null)
						fiw.reportCanonicalInsert();
				} else if("SSK".equals(keyType)) {
					insertURI = new FreenetURI("SSK@");
					if(fiw != null)
						fiw.reportRandomInsert();
				} else if("specify".equals(keyType)) {
					try {
						String u = request.getPartAsStringFailsafe("key", MAX_KEY_LENGTH);
						insertURI = new FreenetURI(u);
						if(logMINOR)
							Logger.minor(this, "Inserting key: "+insertURI+" ("+u+")");
					} catch (MalformedURLException mue1) {
						writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx, false, true);
						return;
					}
				} else {
					writeError(l10n("errorMustSpecifyKeyTypeTitle"),
						   l10n("errorMustSpecifyKeyType"), ctx, false, true);
					return;
				}
				final HTTPUploadedFile file = request.getUploadedFile("filename");
				if (file == null || file.getFilename().trim().isEmpty()) {
					writeError(l10n("errorNoFileSelected"), l10n("errorNoFileSelectedU"), ctx, false, true);
					return;
				}
				final boolean compress = !request.getPartAsStringFailsafe("compress", 128).isEmpty();
				final String identifier = file.getFilename() + "-fred-" + System.currentTimeMillis();
				final String compatibilityMode = request.getPartAsStringFailsafe("compatibilityMode", 100);
				final CompatibilityMode cmode;
				if(compatibilityMode.isEmpty())
					cmode = CompatibilityMode.COMPAT_DEFAULT.intern();
				else
					cmode = CompatibilityMode.valueOf(compatibilityMode).intern();
				String s = request.getPartAsStringFailsafe("overrideSplitfileKey", 65);
				final byte[] overrideSplitfileKey;
				if(s != null && !s.isEmpty())
					overrideSplitfileKey = HexUtil.hexToBytes(s);
				else
					overrideSplitfileKey = null;
				final String fnam;
				if(insertURI.getKeyType().equals("CHK") || keyType.equals("SSK"))
					fnam = file.getFilename();
				else
					fnam = null;
				/* copy bucket data */
				final RandomAccessBucket copiedBucket = core.getPersistentTempBucketFactory().makeBucket(file.getData().size());
				BucketTools.copy(file.getData(), copiedBucket);
				final CountDownLatch done = new CountDownLatch(1);
				try {
					core.getClientLayerPersister().queue(new PersistentJob() {

						@Override
						public String toString() {
							return "QueueToadlet StartInsert";
						}

						@Override
						public boolean run(ClientContext context) {
							try {
							final ClientPut clientPut;
							try {
								clientPut = new ClientPut(fcp.getGlobalForeverClient(), insertURI, identifier, Integer.MAX_VALUE, null, RequestStarter.BULK_SPLITFILE_PRIORITY_CLASS, Persistence.FOREVER, null, false, !compress, -1, UploadFrom.DIRECT, null, file.getContentType(), copiedBucket, null, fnam, false, false, Node.FORK_ON_CACHEABLE_DEFAULT, HighLevelSimpleClientImpl.EXTRA_INSERTS_SINGLE_BLOCK, HighLevelSimpleClientImpl.EXTRA_INSERTS_SPLITFILE_HEADER, false, cmode, overrideSplitfileKey, false, fcp.getCore());
								if(clientPut != null)
									try {
										fcp.startBlocking(clientPut, context);
									} catch (IdentifierCollisionException e) {
										Logger.error(this, "Cannot put same file twice in same millisecond");
										writePermanentRedirect(ctx, "Done", path());
										return false;
									}
								writePermanentRedirect(ctx, "Done", path());
								return true;
							} catch (IdentifierCollisionException e) {
								Logger.error(this, "Cannot put same file twice in same millisecond");
								writePermanentRedirect(ctx, "Done", path());
								return false;
							} catch (NotAllowedException e) {
								writeError(l10n("errorAccessDenied"), l10n("errorAccessDeniedFile", "file", file.getFilename()), ctx, false, true);
								return false;
							} catch (FileNotFoundException e) {
								writeError(l10n("errorNoFileOrCannotRead"), l10n("errorAccessDeniedFile", "file", file.getFilename()), ctx, false, true);
								return false;
							} catch (MalformedURLException mue1) {
								writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx, false, true);
								return false;
							} catch (MetadataUnresolvedException e) {
								Logger.error(this, "Unresolved metadata in starting insert from data uploaded from browser: "+e, e);
								writePermanentRedirect(ctx, "Done", path());
								return false;
								// FIXME should this be a proper localised message? It shouldn't happen... but we'd like to get reports if it does.
							} catch (Throwable t) {
								writeInternalError(t, ctx);
								return false;
							} finally {
								done.countDown();
							}
							} catch (IOException e) {
								// Ignore
								return false;
							} catch (ToadletContextClosedException e) {
								// Ignore
								return false;
							}
						}

					}, NativeThread.HIGH_PRIORITY+1);
				} catch (PersistenceDisabledException e1) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				while (done.getCount() > 0) {
					try {
						done.await();
					} catch (InterruptedException e) {
						// Ignore
					}
				}
				return;
			} else if (request.isPartSet(LocalFileBrowserToadlet.selectFile)) {
				final String filename = request.getPartAsStringFailsafe("filename", MAX_FILENAME_LENGTH);
				if(logMINOR) Logger.minor(this, "Inserting local file: "+filename);
				final File file = new File(filename);
				final String identifier = file.getName() + "-fred-" + System.currentTimeMillis();
				final String contentType = DefaultMIMETypes.guessMIMEType(filename, false);
				final FreenetURI furi;
				final String key = request.getPartAsStringFailsafe("key", MAX_KEY_LENGTH);
				final boolean compress = request.isPartSet("compress");
				final String compatibilityMode = request.getPartAsStringFailsafe("compatibilityMode", 100);
				final CompatibilityMode cmode;
				if(compatibilityMode.isEmpty())
					cmode = CompatibilityMode.COMPAT_DEFAULT;
				else
					cmode = CompatibilityMode.valueOf(compatibilityMode);
				String s = request.getPartAsStringFailsafe("overrideSplitfileKey", 65);
				final byte[] overrideSplitfileKey;
				if(s != null && !s.isEmpty())
					overrideSplitfileKey = HexUtil.hexToBytes(s);
				else
					overrideSplitfileKey = null;
				if(key != null) {
					try {
						furi = new FreenetURI(key);
					} catch (MalformedURLException e) {
						writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx);
						return;
					}
				} else {
					furi = new FreenetURI("CHK@");
				}
				final String target;
				if(furi.getDocName() != null)
					target = null;
				else
					target = file.getName();
				final CountDownLatch done = new CountDownLatch(1);
				try {
					core.getClientLayerPersister().queue(new PersistentJob() {

						@Override
						public String toString() {
							return "QueueToadlet StartLocalFileInsert";
						}

						@Override
						public boolean run(ClientContext context) {
							final ClientPut clientPut;
							try {
							try {
								clientPut = new ClientPut(fcp.getGlobalForeverClient(), furi, identifier, Integer.MAX_VALUE, null, RequestStarter.BULK_SPLITFILE_PRIORITY_CLASS, Persistence.FOREVER, null, false, !compress, -1, UploadFrom.DISK, file, contentType, new FileBucket(file, true, false, false, false), null, target, false, false, Node.FORK_ON_CACHEABLE_DEFAULT, HighLevelSimpleClientImpl.EXTRA_INSERTS_SINGLE_BLOCK, HighLevelSimpleClientImpl.EXTRA_INSERTS_SPLITFILE_HEADER, false, cmode, overrideSplitfileKey, false, fcp.getCore());
								if(logMINOR) Logger.minor(this, "Started global request to insert "+file+" to CHK@ as "+identifier);
								if(clientPut != null)
									try {
										fcp.startBlocking(clientPut, context);
									} catch (IdentifierCollisionException e) {
										Logger.error(this, "Cannot put same file twice in same millisecond");
										writePermanentRedirect(ctx, "Done", path());
										return false;
									} catch (PersistenceDisabledException e) {
										// Impossible???
									}
								writePermanentRedirect(ctx, "Done", path());
								return true;
							} catch (IdentifierCollisionException e) {
								Logger.error(this, "Cannot put same file twice in same millisecond");
								writePermanentRedirect(ctx, "Done", path());
								return false;
							} catch (MalformedURLException e) {
								writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx);
								return false;
							} catch (FileNotFoundException e) {
								writeError(l10n("errorNoFileOrCannotRead"), l10n("errorAccessDeniedFile", "file", target), ctx);
								return false;
							} catch (NotAllowedException e) {
								writeError(l10n("errorAccessDenied"), l10n("errorAccessDeniedFile", new String[]{ "file" }, new String[]{ file.getName() }), ctx);
								return false;
							} catch (MetadataUnresolvedException e) {
								Logger.error(this, "Unresolved metadata in starting insert from data from file: "+e, e);
								writePermanentRedirect(ctx, "Done", path());
								return false;
								// FIXME should this be a proper localised message? It shouldn't happen... but we'd like to get reports if it does.
							} finally {
								done.countDown();
							}
							} catch (IOException e) {
								// Ignore
								return false;
							} catch (ToadletContextClosedException e) {
								// Ignore
								return false;
							}
						}

					}, NativeThread.HIGH_PRIORITY+1);
				} catch (PersistenceDisabledException e1) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				while (done.getCount() > 0) {
					try {
						done.await();
					} catch (InterruptedException e) {
						// Ignore
					}
				}
				return;
			} else if (request.isPartSet(LocalFileBrowserToadlet.selectDir)) {
				final String filename = request.getPartAsStringFailsafe("filename", MAX_FILENAME_LENGTH);
				if(logMINOR) Logger.minor(this, "Inserting local directory: "+filename);
				final File file = new File(filename);
				final String identifier = file.getName() + "-fred-" + System.currentTimeMillis();
				final FreenetURI furi;
				final String key = request.getPartAsStringFailsafe("key", MAX_KEY_LENGTH);
				final boolean compress = request.isPartSet("compress");
				String s = request.getPartAsStringFailsafe("overrideSplitfileKey", 65);
				final byte[] overrideSplitfileKey;
				if(s != null && !s.isEmpty())
					overrideSplitfileKey = HexUtil.hexToBytes(s);
				else
					overrideSplitfileKey = null;
				if(key != null) {
					try {
						furi = new FreenetURI(key);
					} catch (MalformedURLException e) {
						writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx);
						return;
					}
				} else {
					furi = new FreenetURI("CHK@");
				}
				final CountDownLatch done = new CountDownLatch(1);
				try {
					core.getClientLayerPersister().queue(new PersistentJob() {

						@Override
						public String toString() {
							return "QueueToadlet StartLocalDirInsert";
						}

						@Override
						public boolean run(ClientContext context) {
							ClientPutDir clientPutDir;
							try {
								try {
									clientPutDir = new ClientPutDir(fcp.getGlobalForeverClient(), furi, identifier, Integer.MAX_VALUE, RequestStarter.BULK_SPLITFILE_PRIORITY_CLASS, Persistence.FOREVER, null, false, !compress, -1, file, null, false, /* make include hidden files configurable? FIXME */ false, true, false, false, Node.FORK_ON_CACHEABLE_DEFAULT, HighLevelSimpleClientImpl.EXTRA_INSERTS_SINGLE_BLOCK, HighLevelSimpleClientImpl.EXTRA_INSERTS_SPLITFILE_HEADER, false, overrideSplitfileKey, fcp.getCore());
									if(logMINOR) Logger.minor(this, "Started global request to insert dir "+file+" to "+furi+" as "+identifier);
									if(clientPutDir != null) {
										try {
											fcp.startBlocking(clientPutDir, context);
										} catch (IdentifierCollisionException e) {
											Logger.error(this, "Cannot put same file twice in same millisecond");
											writePermanentRedirect(ctx, "Done", path());
											return false;
										} catch (PersistenceDisabledException e) {
											sendPersistenceDisabledError(ctx);
											return false;
										}
									}
									writePermanentRedirect(ctx, "Done", path());
									return true;
								} catch (IdentifierCollisionException e) {
									Logger.error(this, "Cannot put same directory twice in same millisecond");
									writePermanentRedirect(ctx, "Done", path());
									return false;
								} catch (MalformedURLException e) {
									writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx);
									return false;
								} catch (FileNotFoundException e) {
									writeError(l10n("errorNoFileOrCannotRead"), l10n("errorAccessDeniedFile", "file", file.toString()), ctx);
									return false;
								} catch (TooManyFilesInsertException e) {
									writeError(l10n("tooManyFilesInOneFolder"), l10n("tooManyFilesInOneFolder"), ctx);
									return false;
								} finally {
									done.countDown();
								}
							} catch (IOException e) {
								// Ignore
								return false;
							} catch (ToadletContextClosedException e) {
								// Ignore
								return false;
							}
						}

					}, NativeThread.HIGH_PRIORITY+1);
				} catch (PersistenceDisabledException e1) {
					sendPersistenceDisabledError(ctx);
					return;
				}
				while (done.getCount() > 0) {
					try {
						done.await();
					} catch (InterruptedException e) {
						// Ignore
					}
				}
				return;
			} else if (request.isPartSet("recommend_request")) {
				PageNode page = ctx.getPageMaker().getPageNode(l10n("recommendAFileToFriends"), ctx);
				HTMLNode pageNode = page.outer;
				HTMLNode contentNode = page.content;
				HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("#", l10n("recommendAFileToFriends"), contentNode, "recommend-file", true);
				HTMLNode form = ctx.addFormChild(infoboxContent, path(), "recommendForm2");
				
				int x = 0;
				for(String part : request.getParts()) {
					if(!part.startsWith("identifier-")) continue;
					String key = request.getPartAsStringFailsafe("key-"+part.substring("identifier-".length()), MAX_KEY_LENGTH);
					if(key == null || key.isEmpty()) {
						continue;
					}
					form.addChild("#", l10n("key") + ":");
					form.addChild("br");
					form.addChild("#", key);
					form.addChild("br");
					form.addChild("input", new String[] { "type", "name", "value" },
							new String[] { "hidden", "key-"+x, key });
					x += 1;
				}
				form.addChild("label", "for", "descB", (l10n("recommendDescription") + ' '));
				form.addChild("br");
				form.addChild("textarea",
					new String[]{"id", "name", "row", "cols"},
					new String[]{"descB", "description", "3", "70"});
				form.addChild("br");
				if (core.getNode().isFProxyJavascriptEnabled()) {
					form.addChild("script", new String[] {"type", "src"}, new String[] {"text/javascript", "/static/js/checkall.js"});
				}
				HTMLNode peerTable = form.addChild("table", "class", "darknet_connections");
				if (core.getNode().isFProxyJavascriptEnabled()) {
					HTMLNode headerRow = peerTable.addChild("tr");
					headerRow.addChild("th").addChild("input", new String[] { "type", "onclick" }, new String[] { "checkbox", "checkAll(this, 'darknet_connections')" });
					headerRow.addChild("th", l10n("recommendToFriends"));
				} else {
					peerTable.addChild("tr").addChild("th", "colspan", "2", l10n("recommendToFriends"));
				}
				for(DarknetPeerNode peer : core.getNode().getDarknetConnections()) {
					HTMLNode peerRow = peerTable.addChild("tr", "class", "darknet_connections_normal");
					peerRow.addChild("td", "class", "peer-marker").addChild("input",
						new String[] { "type", "name" }, 
						new String[] { "checkbox", "node_" + peer.hashCode() });
					peerRow.addChild("td", "class", "peer-name").addChild("#", peer.getName());
				}

				form.addChild("input",
					new String[]{"type", "name", "value"},
					new String[]{"submit", "recommend_uri", l10n("recommend")});

				this.writeHTMLReply(ctx, 200, "OK", pageNode.generate());
				return;
			} else if(request.isPartSet("recommend_uri")) {
				String description = request.getPartAsStringFailsafe("description", 32768);
				ArrayList<FreenetURI> uris = new ArrayList<FreenetURI>();
				for(String part : request.getParts()) {
					if(!part.startsWith("key-")) continue;
					String key = request.getPartAsStringFailsafe(part, MAX_KEY_LENGTH);
					try {
						FreenetURI furi = new FreenetURI(key);
						uris.add(furi);
					} catch (MalformedURLException e) {
						writeError(l10n("errorInvalidURI"), l10n("errorInvalidURIToU"), ctx);
						return;
					}
				}
				
				for(DarknetPeerNode peer : core.getNode().getDarknetConnections()) {
					if(request.isPartSet("node_" + peer.hashCode())) {
						for(FreenetURI furi : uris)
							peer.sendDownloadFeed(furi, description);
					}
				}
				writePermanentRedirect(ctx, "Done", path());
				return;
			}
		} finally {
			request.freeParts();
		}
		this.handleMethodGET(uri, new HTTPRequestImpl(uri, "GET"), ctx);
	}

	private void handleChangePriority(HTTPRequest request, ToadletContext ctx, String suffix) throws ToadletContextClosedException, IOException {
		short newPriority = Short.parseShort(request.getPartAsStringFailsafe("priority"+suffix, 32));
		String identifier = "";
		for(String part : request.getParts()) {
			if(!part.startsWith("identifier-")) continue;
			identifier = part.substring("identifier-".length());
			if(identifier.length() > 50) continue;
			identifier = request.getPartAsStringFailsafe(part, MAX_IDENTIFIER_LENGTH);
			try {
				fcp.modifyGlobalRequestBlocking(identifier, null, newPriority);
			} catch (PersistenceDisabledException e) {
				sendPersistenceDisabledError(ctx);
				return;
			}
		}
		writePermanentRedirect(ctx, "Done", path());
	}

	private void downloadDisallowedPage (NotAllowedException e, String downloadPath, ToadletContext ctx)
		throws IOException, ToadletContextClosedException {
		PageNode page = ctx.getPageMaker().getPageNode(l10n("downloadFiles"), ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;
		Logger.warning(this, e.toString());
		HTMLNode alert = ctx.getPageMaker().getInfobox("infobox-alert",
			l10n("downloadFiles"), contentNode, "grouped-downloads", true);
		alert.addChild("ul", l10n("downloadDisallowed", "directory", downloadPath));
		alert.addChild("a", "href", path(),
			NodeL10n.getBase().getString("Toadlet.returnToQueuepage"));
		writeHTMLReply(ctx, 200, "OK", pageNode.generate());
	}

	private File getDownloadsDir (String downloadPath) throws NotAllowedException {
		File downloadsDir = new File(downloadPath);
		//Invalid if it's disallowed, doesn't exist, isn't a directory, or can't be created.
		if(!core.allowDownloadTo(downloadsDir) || !((downloadsDir.exists() && 
				downloadsDir.isDirectory()) || !downloadsDir.mkdirs())) {
			throw new NotAllowedException();
		}
		return downloadsDir;
	}

	private void sendPanicingPage(ToadletContext ctx) throws ToadletContextClosedException, IOException {
		writeHTMLReply(ctx, 200, "OK", WelcomeToadlet.sendRestartingPageInner(ctx).generate());
	}

	private void sendConfirmPanicPage(ToadletContext ctx) throws ToadletContextClosedException, IOException {
		PageNode page = ctx.getPageMaker().getPageNode(l10n("confirmPanicButtonPageTitle"), ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;

		HTMLNode content = ctx.getPageMaker().getInfobox("infobox-error",
			l10n("confirmPanicButtonPageTitle"), contentNode, "confirm-panic", true).
			addChild("div", "class", "infobox-content");

		content.addChild("p", l10n("confirmPanicButton"));

		HTMLNode form = ctx.addFormChild(content, path(), "confirmPanicButton");
		form.addChild("p").addChild("input",
			new String[] { "type", "name", "value" },
			new String[] { "submit", "confirmpanic", l10n("confirmPanicButtonYes") });
		form.addChild("p").addChild("input",
			new String[] { "type", "name", "value" },
			new String[] { "submit", "noconfirmpanic", l10n("confirmPanicButtonNo") });

		if(uploads)
			content.addChild("p").addChild("a", "href", path(), l10n("backToUploadsPage"));
		else
			content.addChild("p").addChild("a", "href", path(), l10n("backToDownloadsPage"));

		writeHTMLReply(ctx, 200, "OK", pageNode.generate());
	}

	private void sendPersistenceDisabledError(ToadletContext ctx) throws ToadletContextClosedException, IOException {
		String title = l10n("awaitingPasswordTitle"+(uploads ? "Uploads" : "Downloads"));
		if(core.getNode().awaitingPassword()) {
			PageNode page = ctx.getPageMaker().getPageNode(title, ctx);
			HTMLNode pageNode = page.outer;
			HTMLNode contentNode = page.content;

			HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("infobox-error", title, contentNode, null, true);

			SecurityLevelsToadlet.generatePasswordFormPage(false, container, infoboxContent, false, false, false, null, path());

			addHomepageLink(infoboxContent);

			writeHTMLReply(ctx, 500, "Internal Server Error", pageNode.generate());
			return;

		}
		if(core.getNode().isStopping())
			sendErrorPage(ctx, 200,
					l10n("shuttingDownTitle"),
					l10n("shuttingDown"));
		else
			sendErrorPage(ctx, 200,
					l10n("persistenceBrokenTitle"),
					l10n("persistenceBroken",
							new String[]{ "TEMPDIR", "DBFILE" },
							new String[]{ FileUtil.getCanonicalFile(core.getPersistentTempDir()).toString()+File.separator, core.getNode().getDatabasePath() }
					));
	}

	private void writeError(String header, String message, ToadletContext context) throws ToadletContextClosedException, IOException {
		writeError(header, message, context, true, false);
	}

	private void writeError(String header, String message, ToadletContext context, boolean returnToQueuePage, boolean returnToInsertPage) throws ToadletContextClosedException, IOException {
		PageMaker pageMaker = context.getPageMaker();
		PageNode page = pageMaker.getPageNode(header, context);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;
		if(context.isAllowedFullAccess())
			contentNode.addChild(context.getAlertManager().createSummary());
		HTMLNode infoboxContent = pageMaker.getInfobox("infobox-error", header, contentNode, "queue-error", false);
		infoboxContent.addChild("#", message);
		if(returnToQueuePage)
			NodeL10n.getBase().addL10nSubstitution(infoboxContent.addChild("div"), "QueueToadlet.returnToQueuePage", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(path()) });
		else if(returnToInsertPage)
			NodeL10n.getBase().addL10nSubstitution(infoboxContent.addChild("div"), "QueueToadlet.tryAgainUploadFilePage", new String[] { "link" }, new HTMLNode[] { HTMLNode.link(FileInsertWizardToadlet.PATH) });
		writeHTMLReply(context, 400, "Bad request", pageNode.generate());
	}

	public void handleMethodGET(URI uri, final HTTPRequest request, final ToadletContext ctx)
	throws ToadletContextClosedException, IOException, RedirectException {

		// We ensure that we have a FCP server running
		if(!fcp.isEnabled()){
			writeError(l10n("fcpIsMissing"), l10n("pleaseEnableFCP"), ctx, false, false);
			return;
		}

		if(container.publicGatewayMode() && !ctx.isAllowedFullAccess()) {
		    sendUnauthorizedPage(ctx);
			return;
		}

		final String requestPath = request.getPath().substring(path().length());

		boolean countRequests = false;
		boolean listKeys = false;

		if (!requestPath.isEmpty()) {
			if(requestPath.equals("countRequests.html") || requestPath.equals("/countRequests.html")) {
				countRequests = true;
			} else if(requestPath.equals(KEY_LIST_LOCATION)) {
				listKeys = true;
			}
		}

		class OutputWrapper {
			boolean done;
			HTMLNode pageNode;
			String plainText;
		}

		final OutputWrapper ow = new OutputWrapper();

		final PageMaker pageMaker = ctx.getPageMaker();

		final boolean count = countRequests;
		final boolean keys = listKeys;
		
		if(!(count || keys)) {
			try {
				RequestStatus[] reqs = fcp.getGlobalRequests();
				HTMLNode pageNode = handleGetInner(pageMaker, reqs, core.clientContext, request, ctx);
				writeHTMLReply(ctx, 200, "OK", new MultiValueTable<>(), pageNode.generate());
				return;
			} catch (PersistenceDisabledException e) {
				sendPersistenceDisabledError(ctx);
				return;
			}
		}

		try {
			core.getClientContext().jobRunner.queue(new PersistentJob() {

				@Override
				public String toString() {
					return "QueueToadlet ShowQueue";
				}

				@Override
				public boolean run(ClientContext context) {
					HTMLNode pageNode = null;
					String plainText = null;
					try {
						if(count) {
							long queued = core.getRequestStarters().chkFetchSchedulerBulk.countPersistentWaitingKeys() + core.getRequestStarters().chkFetchSchedulerRT.countPersistentWaitingKeys();
							Logger.minor(this, "Total waiting CHKs: "+queued);
							long reallyQueued = core.getRequestStarters().chkFetchSchedulerBulk.countQueuedRequests() + core.getRequestStarters().chkFetchSchedulerRT.countQueuedRequests();
							Logger.minor(this, "Total queued CHK requests (including transient): "+reallyQueued);
							PageNode page = pageMaker.getPageNode(l10n("title"), ctx);
							pageNode = page.outer;
							HTMLNode contentNode = page.content;
							/* add alert summary box */
							if(ctx.isAllowedFullAccess())
								contentNode.addChild(ctx.getAlertManager().createSummary());
							HTMLNode infoboxContent = pageMaker.getInfobox("infobox-information", "Queued requests status", contentNode, null, false);
							infoboxContent.addChild("p", "Total awaiting CHKs: "+queued);
							infoboxContent.addChild("p", "Total queued CHK requests: "+reallyQueued);
							return false;
						} else /*if(keys)*/ {
							try {
								plainText = makeKeysList(context, uploads);
							} catch (PersistenceDisabledException e) {
								plainText = null;
							}
							return false;
						}
					} finally {
						synchronized(ow) {
							ow.done = true;
							ow.pageNode = pageNode;
							ow.plainText = plainText;
							ow.notifyAll();
						}
					}
				}
			// Do not use maximal priority: There may be exceptional cases which have higher priority than the UI, to get rid of excessive garbage for example.
			}, NativeThread.HIGH_PRIORITY);
		} catch (PersistenceDisabledException e1) {
			sendPersistenceDisabledError(ctx);
			return;
		}

		HTMLNode pageNode;
		String plainText;
		synchronized(ow) {
			while(true) {
				if(ow.done) {
					pageNode = ow.pageNode;
					plainText = ow.plainText;
					break;
				}
				try {
					ow.wait();
				} catch (InterruptedException e) {
					// Ignore
				}
			}
		}

		if (pageNode != null) {
			writeHTMLReply(ctx, 200, "OK", new MultiValueTable<>(), pageNode.generate());
		} else if (plainText != null) {
			this.writeReply(ctx, 200, "text/plain", "OK", plainText);
		} else {
			if (core.killedDatabase()) {
				sendPersistenceDisabledError(ctx);
			} else {
				this.writeError("Internal error", "Internal error", ctx);
			}
		}
	}

	protected String makeKeysList(ClientContext context, boolean inserts) throws PersistenceDisabledException {
		RequestStatus[] reqs = fcp.getGlobalRequests();

		StringBuilder sb = new StringBuilder();

		for(RequestStatus req: reqs) {
			if(!inserts && req instanceof DownloadRequestStatus) {
				DownloadRequestStatus get = (DownloadRequestStatus)req;
				FreenetURI uri = get.getURI();
				sb.append(uri.toString());
				sb.append("\n");
			} else if (inserts && req instanceof UploadRequestStatus) {
		UploadRequestStatus put = (UploadRequestStatus)req;
				FreenetURI uri = put.getURI();
		if (uri != null) {
		    sb.append(uri.toString());
		    sb.append("\n");
		}
	    }
		}
		return sb.toString();
	}

	private HTMLNode handleGetInner(PageMaker pageMaker, RequestStatus[] reqs, ClientContext context, final HTTPRequest request, ToadletContext ctx) throws PersistenceDisabledException {

		// First, get the queued requests, and separate them into different types.
		LinkedList<DownloadRequestStatus> completedDownloadToDisk = new LinkedList<DownloadRequestStatus>();
		LinkedList<DownloadRequestStatus> completedDownloadToTemp = new LinkedList<DownloadRequestStatus>();
		LinkedList<UploadFileRequestStatus> completedUpload = new LinkedList<UploadFileRequestStatus>();
		LinkedList<UploadDirRequestStatus> completedDirUpload = new LinkedList<UploadDirRequestStatus>();

		LinkedList<DownloadRequestStatus> failedDownload = new LinkedList<DownloadRequestStatus>();
		LinkedList<UploadFileRequestStatus> failedUpload = new LinkedList<UploadFileRequestStatus>();
		LinkedList<UploadDirRequestStatus> failedDirUpload = new LinkedList<UploadDirRequestStatus>();

		LinkedList<DownloadRequestStatus> uncompletedDownload = new LinkedList<DownloadRequestStatus>();
		LinkedList<UploadFileRequestStatus> uncompletedUpload = new LinkedList<UploadFileRequestStatus>();
		LinkedList<UploadDirRequestStatus> uncompletedDirUpload = new LinkedList<UploadDirRequestStatus>();

		Map<String, LinkedList<DownloadRequestStatus>> failedUnknownMIMEType = new HashMap<String, LinkedList<DownloadRequestStatus>>();
		Map<String, LinkedList<DownloadRequestStatus>> failedBadMIMEType = new HashMap<String, LinkedList<DownloadRequestStatus>>();

		if(logMINOR)
			Logger.minor(this, "Request count: "+reqs.length);

		if(reqs.length < 1){
		    return sendEmptyQueuePage(ctx, pageMaker);
		}

		short lowestQueuedPrio = RequestStarter.PAUSED_PRIORITY_CLASS;

		long totalQueuedDownloadSize = 0;
		long totalQueuedUploadSize = 0;

		boolean added = false;
		for(RequestStatus req: reqs) {
			if(req instanceof DownloadRequestStatus && !uploads) {
				DownloadRequestStatus download = (DownloadRequestStatus)req;
				if(download.hasSucceeded()) {
					if(download.toTempSpace())
						completedDownloadToTemp.add(download);
					else // to disk
						completedDownloadToDisk.add(download);
				} else if(download.hasFinished()) {
				    FetchExceptionMode failureCode = download.getFailureCode();
					String mimeType = download.getMIMEType();
					if(mimeType == null && (failureCode == FetchExceptionMode.CONTENT_VALIDATION_UNKNOWN_MIME || failureCode == FetchExceptionMode.CONTENT_VALIDATION_BAD_MIME)) {
						Logger.error(this, "MIME type is null but failure code is "+FetchException.getMessage(failureCode)+" for "+download.getIdentifier()+" : "+download.getURI());
						mimeType = DefaultMIMETypes.DEFAULT_MIME_TYPE;
					}
					if(failureCode == FetchExceptionMode.CONTENT_VALIDATION_UNKNOWN_MIME) {
						mimeType = ContentFilter.stripMIMEType(mimeType);
						LinkedList<DownloadRequestStatus> list = failedUnknownMIMEType.get(mimeType);
						if(list == null) {
							list = new LinkedList<DownloadRequestStatus>();
							failedUnknownMIMEType.put(mimeType, list);
						}
						list.add(download);
					} else if(failureCode == FetchExceptionMode.CONTENT_VALIDATION_BAD_MIME) {
						mimeType = ContentFilter.stripMIMEType(mimeType);
						FilterMIMEType type = ContentFilter.getMIMEType(mimeType);
						LinkedList<DownloadRequestStatus> list;
						if(type == null) {
							Logger.error(this, "Bad MIME failure code yet MIME is "+mimeType+" which does not have a handler!");
							list = failedUnknownMIMEType.get(mimeType);
							if(list == null) {
								list = new LinkedList<DownloadRequestStatus>();
								failedUnknownMIMEType.put(mimeType, list);
							}
						} else {
							list = failedBadMIMEType.get(mimeType);
							if(list == null) {
								list = new LinkedList<DownloadRequestStatus>();
								failedBadMIMEType.put(mimeType, list);
							}
						}
						list.add(download);
					} else {
						failedDownload.add(download);
					}
				} else {
					short prio = download.getPriority();
					if(prio < lowestQueuedPrio)
						lowestQueuedPrio = prio;
					uncompletedDownload.add(download);
					long size = download.getDataSize();
					if(size > 0)
						totalQueuedDownloadSize += size;
				}
				added = true;
			} else if(req instanceof UploadFileRequestStatus && uploads) {
				UploadFileRequestStatus upload = (UploadFileRequestStatus)req;
				if(upload.hasSucceeded()) {
					completedUpload.add(upload);
				} else if(upload.hasFinished()) {
					failedUpload.add(upload);
				} else {
					short prio = upload.getPriority();
					if(prio < lowestQueuedPrio)
						lowestQueuedPrio = prio;
					uncompletedUpload.add(upload);
				}
				long size = upload.getDataSize();
				if(size > 0)
					totalQueuedUploadSize += size;
				added = true;
			} else if(req instanceof UploadDirRequestStatus && uploads) {
				UploadDirRequestStatus upload = (UploadDirRequestStatus)req;
				if(upload.hasSucceeded()) {
					completedDirUpload.add(upload);
				} else if(upload.hasFinished()) {
					failedDirUpload.add(upload);
				} else {
					short prio = upload.getPriority();
					if(prio < lowestQueuedPrio)
						lowestQueuedPrio = prio;
					uncompletedDirUpload.add(upload);
				}
				long size = upload.getTotalDataSize();
				if(size > 0)
					totalQueuedUploadSize += size;
		added = true;
			}
		}
		if(!added) {
		    return sendEmptyQueuePage(ctx, pageMaker);
		}
		Logger.minor(this, "Total queued downloads: "+SizeUtil.formatSize(totalQueuedDownloadSize));
		Logger.minor(this, "Total queued uploads: "+SizeUtil.formatSize(totalQueuedUploadSize));

		Comparator<RequestStatus> jobComparator = new Comparator<RequestStatus>() {
			@Override
			public int compare(RequestStatus firstRequest, RequestStatus secondRequest) {
				
				if(firstRequest == secondRequest) return 0; // Short cut.
				
				int result = 0;
				boolean isSet = true;

				if(request.isParameterSet("sortBy")){
					final String sortBy = request.getParam("sortBy");

					switch (sortBy) {
						case "id":
							result = firstRequest.getIdentifier().compareToIgnoreCase(secondRequest.getIdentifier());
							if(result == 0)
								result = firstRequest.getIdentifier().compareTo(secondRequest.getIdentifier());
							break;
						case "size":
							result = Fields.compare(firstRequest.getTotalBlocks(), secondRequest.getTotalBlocks());
							break;
						case "progress":
							boolean firstFinalized = firstRequest.isTotalFinalized();
							boolean secondFinalized = secondRequest.isTotalFinalized();
							if(firstFinalized && !secondFinalized)
								result = 1;
							else if(secondFinalized && !firstFinalized)
								result = -1;
							else {
								double firstProgress = ((double)firstRequest.getFetchedBlocks()) / ((double)firstRequest.getMinBlocks());
								double secondProgress = ((double)secondRequest.getFetchedBlocks()) / ((double)secondRequest.getMinBlocks());
								result = Fields.compare(firstProgress, secondProgress);
							}
							break;
						case "lastActivity":
							result = Fields.compare(firstRequest.getLastSuccess(),
									secondRequest.getLastSuccess());
							break;
						case "lastFailure":
							result = Fields.compare(firstRequest.getLastFailure(),
									secondRequest.getLastFailure());
							break;
						default:
							isSet=false;
							break;
					}
				}else
					isSet=false;

				if(!isSet){
					result = Fields.compare(firstRequest.getPriority(), secondRequest.getPriority());
					if(result == 0)
						result = firstRequest.getIdentifier().compareTo(secondRequest.getIdentifier());
				}

				if(result == 0){
					return 0;
				}else if(request.isParameterSet("reversed")){
					isReversed = true;
					return result > 0 ? -1 : 1;
				}else{
					isReversed = false;
					return result < 0 ? -1 : 1;
				}
			}
		};

		Collections.sort(completedDownloadToDisk, jobComparator);
		Collections.sort(completedDownloadToTemp, jobComparator);
		Collections.sort(completedUpload, jobComparator);
		Collections.sort(completedDirUpload, jobComparator);
		Collections.sort(failedDownload, jobComparator);
		Collections.sort(failedUpload, jobComparator);
		Collections.sort(failedDirUpload, jobComparator);
		Collections.sort(uncompletedDownload, jobComparator);
		Collections.sort(uncompletedUpload, jobComparator);
		Collections.sort(uncompletedDirUpload, jobComparator);

		String pageName;
		if(uploads)
			pageName =
				"(" + (uncompletedDirUpload.size() + uncompletedUpload.size()) +
				'/' + (failedDirUpload.size() + failedUpload.size()) +
				'/' + (completedDirUpload.size() + completedUpload.size()) +
				") "+l10n("titleUploads");
		else
			pageName =
				"(" + uncompletedDownload.size() +
				'/' + failedDownload.size() +
				'/' + (completedDownloadToDisk.size() + completedDownloadToTemp.size()) +
				") "+l10n("titleDownloads");

		PageNode page = pageMaker.getPageNode(pageName, ctx);
		HTMLNode pageNode = page.outer;
		HTMLNode contentNode = page.content;

		/* add alert summary box */
		if(ctx.isAllowedFullAccess())
			contentNode.addChild(ctx.getAlertManager().createSummary());

		/* navigation bar */
		InfoboxNode infobox = pageMaker.getInfobox("navbar", l10n("requestNavigation"), null, false);
		HTMLNode navigationBar = infobox.outer;
		HTMLNode navigationContent = infobox.content.addChild("ul");
		boolean includeNavigationBar = false;
		if (!completedDownloadToTemp.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#completedDownloadToTemp", l10n("completedDtoTemp", new String[]{ "size" }, new String[]{ String.valueOf(completedDownloadToTemp.size()) }));
			includeNavigationBar = true;
		}
		if (!completedDownloadToDisk.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#completedDownloadToDisk", l10n("completedDtoDisk", new String[]{ "size" }, new String[]{ String.valueOf(completedDownloadToDisk.size()) }));
			includeNavigationBar = true;
		}
		if (!completedUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#completedUpload", l10n("completedU", new String[]{ "size" }, new String[]{ String.valueOf(completedUpload.size()) }));
			includeNavigationBar = true;
		}
		if (!completedDirUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#completedDirUpload", l10n("completedDU", new String[]{ "size" }, new String[]{ String.valueOf(completedDirUpload.size()) }));
			includeNavigationBar = true;
		}
		if (!failedDownload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#failedDownload", l10n("failedD", new String[]{ "size" }, new String[]{ String.valueOf(failedDownload.size()) }));
			includeNavigationBar = true;
		}
		if (!failedUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#failedUpload", l10n("failedU", new String[]{ "size" }, new String[]{ String.valueOf(failedUpload.size()) }));
			includeNavigationBar = true;
		}
		if (!failedDirUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#failedDirUpload", l10n("failedDU", new String[]{ "size" }, new String[]{ String.valueOf(failedDirUpload.size()) }));
			includeNavigationBar = true;
		}
		if (!failedUnknownMIMEType.isEmpty()) {
			String[] types = failedUnknownMIMEType.keySet().toArray(new String[failedUnknownMIMEType.size()]);
			Arrays.sort(types);
			for(String type : types) {
				String atype = type.replace("-", "--").replace('/', '-');
				navigationContent.addChild("li").addChild("a", "href", "#failedDownload-unknowntype-"+atype, l10n("failedDUnknownMIME", new String[]{ "size", "type" }, new String[]{ String.valueOf(failedUnknownMIMEType.get(type).size()), type }));
			}
		}
		if (!failedBadMIMEType.isEmpty()) {
			String[] types = failedBadMIMEType.keySet().toArray(new String[failedBadMIMEType.size()]);
			Arrays.sort(types);
			for(String type : types) {
				String atype = type.replace("-", "--").replace('/', '-');
				navigationContent.addChild("li").addChild("a", "href", "#failedDownload-badtype-"+atype, l10n("failedDBadMIME", new String[]{ "size", "type" }, new String[]{ String.valueOf(failedBadMIMEType.get(type).size()), type }));
			}
		}
		if (!uncompletedDownload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#uncompletedDownload", l10n("DinProgress", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedDownload.size()) }));
			includeNavigationBar = true;
		}
		if (!uncompletedUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#uncompletedUpload", l10n("UinProgress", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedUpload.size()) }));
			includeNavigationBar = true;
		}
		if (!uncompletedDirUpload.isEmpty()) {
			navigationContent.addChild("li").addChild("a", "href", "#uncompletedDirUpload", l10n("DUinProgress", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedDirUpload.size()) }));
			includeNavigationBar = true;
		}
		if (totalQueuedDownloadSize > 0) {
			navigationContent.addChild("li", l10n("totalQueuedDownloads", "size", SizeUtil.formatSize(totalQueuedDownloadSize)));
			includeNavigationBar = true;
		}
		if (totalQueuedUploadSize > 0) {
			navigationContent.addChild("li", l10n("totalQueuedUploads", "size", SizeUtil.formatSize(totalQueuedUploadSize)));
			includeNavigationBar = true;
		}

	navigationContent.addChild("li").addChild("a", "href", KEY_LIST_LOCATION,
						  l10n("openKeyList"));

		if (includeNavigationBar) {
			contentNode.addChild(navigationBar);
		}

		final String[] priorityClasses = new String[] {
				l10n("priority0"),
				l10n("priority1"),
				l10n("priority2"),
				l10n("priority3"),
				l10n("priority4"),
				l10n("priority5"),
				l10n("priority6")
		};

		boolean advancedModeEnabled = pageMaker.advancedMode(request, this.container);

		HTMLNode legendContent = pageMaker.getInfobox("legend", l10n("legend"), contentNode, "queue-legend", true);
		HTMLNode legendTable = legendContent.addChild("table", "class", "queue");
		HTMLNode legendRow = legendTable.addChild("tr");
		for(int i=0; i<7; i++){
		    if(i > RequestStarter.INTERACTIVE_PRIORITY_CLASS || advancedModeEnabled || i <= lowestQueuedPrio)
			legendRow.addChild("td", "class", "priority" + i, priorityClasses[i]);
		}

		if (SimpleToadletServer.isPanicButtonToBeShown) {
		    // There may be persistent downloads etc under other PersistentRequestClient's, so still show it.
			contentNode.addChild(createPanicBox(pageMaker, ctx));
		}

		final QueueColumn[] advancedModeFailure = new QueueColumn[] {
			QueueColumn.IDENTIFIER,
			QueueColumn.FILENAME,
			QueueColumn.SIZE,
			QueueColumn.MIME_TYPE,
			QueueColumn.PROGRESS,
			QueueColumn.REASON,
			QueueColumn.PERSISTENCE,
			QueueColumn.KEY };
		
		final QueueColumn[] simpleModeFailure = new QueueColumn[] {
			QueueColumn.FILENAME,
			QueueColumn.SIZE,
			QueueColumn.PROGRESS,
			QueueColumn.REASON,
			QueueColumn.KEY };

		if (!completedDownloadToTemp.isEmpty()) {
			contentNode.addChild("a", "id", "completedDownloadToTemp");
			HTMLNode completedDownloadsToTempContent = pageMaker.getInfobox("completed_requests", l10n("completedDinTempDirectory", new String[]{ "size" }, new String[]{ String.valueOf(completedDownloadToTemp.size()) }), contentNode, "request-completed", false);
			if (advancedModeEnabled) {
				completedDownloadsToTempContent.addChild(createRequestTable(pageMaker, ctx, completedDownloadToTemp, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.SIZE, QueueColumn.MIME_TYPE, QueueColumn.PERSISTENCE, QueueColumn.KEY, QueueColumn.COMPAT_MODE }, priorityClasses, advancedModeEnabled, "completed-temp", QueueType.CompletedDownloadToTemp));
			} else {
				completedDownloadsToTempContent.addChild(createRequestTable(pageMaker, ctx, completedDownloadToTemp, new QueueColumn[] { QueueColumn.SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-temp", QueueType.CompletedDownloadToTemp));
			}
		}

		if (!completedDownloadToDisk.isEmpty()) {
			contentNode.addChild("a", "id", "completedDownloadToDisk");
			HTMLNode completedToDiskInfoboxContent = pageMaker.getInfobox("completed_requests", l10n("completedDinDownloadDirectory", new String[]{ "size" }, new String[]{ String.valueOf(completedDownloadToDisk.size()) }), contentNode, "request-completed", false);
			if (advancedModeEnabled) {
				completedToDiskInfoboxContent.addChild(createRequestTable(pageMaker, ctx, completedDownloadToDisk, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.MIME_TYPE, QueueColumn.PERSISTENCE, QueueColumn.KEY, QueueColumn.COMPAT_MODE }, priorityClasses, advancedModeEnabled, "completed-disk", QueueType.CompletedDownloadToDisk));
			} else {
				completedToDiskInfoboxContent.addChild(createRequestTable(pageMaker, ctx, completedDownloadToDisk, new QueueColumn[] { QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-disk", QueueType.CompletedDownloadToDisk));
			}
		}

		if (!completedUpload.isEmpty()) {
			contentNode.addChild("a", "id", "completedUpload");
			HTMLNode completedUploadInfoboxContent = pageMaker.getInfobox("completed_requests", l10n("completedU", new String[]{ "size" }, new String[]{ String.valueOf(completedUpload.size()) }), contentNode, "download-completed", false);
			if (advancedModeEnabled) {
				completedUploadInfoboxContent.addChild(createRequestTable(pageMaker, ctx, completedUpload, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.MIME_TYPE, QueueColumn.PERSISTENCE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-upload-file", QueueType.CompletedUpload));
			} else {
				completedUploadInfoboxContent.addChild(createRequestTable(pageMaker, ctx, completedUpload, new QueueColumn[] { QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-upload-file", QueueType.CompletedUpload));
			}
		}

		if (!completedDirUpload.isEmpty()) {
			contentNode.addChild("a", "id", "completedDirUpload");
			HTMLNode completedUploadDirContent = pageMaker.getInfobox("completed_requests", l10n("completedUDirectory", new String[]{ "size" }, new String[]{ String.valueOf(completedDirUpload.size()) }), contentNode, "download-completed", false);
			if (advancedModeEnabled) {
				completedUploadDirContent.addChild(createRequestTable(pageMaker, ctx, completedDirUpload, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILES, QueueColumn.TOTAL_SIZE, QueueColumn.PERSISTENCE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-upload-dir", QueueType.CompletedDirUpload));
			} else {
				completedUploadDirContent.addChild(createRequestTable(pageMaker, ctx, completedDirUpload, new QueueColumn[] { QueueColumn.FILES, QueueColumn.TOTAL_SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "completed-upload-dir", QueueType.CompletedDirUpload));
			}
		}

		if (!failedDownload.isEmpty()) {
			contentNode.addChild("a", "id", "failedDownload");
			HTMLNode failedContent = pageMaker.getInfobox("failed_requests", l10n("failedD", new String[]{ "size" }, new String[]{ String.valueOf(failedDownload.size()) }), contentNode, "download-failed", false);
			if (advancedModeEnabled) {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedDownload, advancedModeFailure, priorityClasses, advancedModeEnabled, "failed-download", QueueType.FailedDownload));
			} else {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedDownload, simpleModeFailure, priorityClasses, advancedModeEnabled, "failed-download", QueueType.FailedDownload));
			}
		}

		if (!failedUpload.isEmpty()) {
			contentNode.addChild("a", "id", "failedUpload");
			HTMLNode failedContent = pageMaker.getInfobox("failed_requests", l10n("failedU", new String[]{ "size" }, new String[]{ String.valueOf(failedUpload.size()) }), contentNode, "upload-failed", false);
			if (advancedModeEnabled) {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedUpload, advancedModeFailure, priorityClasses, advancedModeEnabled, "failed-upload-file", QueueType.FailedUpload));
			} else {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedUpload, simpleModeFailure, priorityClasses, advancedModeEnabled, "failed-upload-file", QueueType.FailedUpload));
			}
		}

		if (!failedDirUpload.isEmpty()) {
			contentNode.addChild("a", "id", "failedDirUpload");
			HTMLNode failedContent = pageMaker.getInfobox("failed_requests", l10n("failedU", new String[]{ "size" }, new String[]{ String.valueOf(failedDirUpload.size()) }), contentNode, "upload-failed", false);
			if (advancedModeEnabled) {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedDirUpload, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILES, QueueColumn.TOTAL_SIZE, QueueColumn.PROGRESS, QueueColumn.REASON, QueueColumn.PERSISTENCE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-upload-dir", QueueType.FailedDirUpload));
			} else {
				failedContent.addChild(createRequestTable(pageMaker, ctx, failedDirUpload, new QueueColumn[] { QueueColumn.FILES, QueueColumn.TOTAL_SIZE, QueueColumn.PROGRESS, QueueColumn.REASON, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-upload-dir", QueueType.FailedDirUpload));
			}
		}

		if(!failedBadMIMEType.isEmpty()) {
			String[] types = failedBadMIMEType.keySet().toArray(new String[failedBadMIMEType.size()]);
			Arrays.sort(types);
			for(String type : types) {
				LinkedList<DownloadRequestStatus> getters = failedBadMIMEType.get(type);
				String atype = type.replace("-", "--").replace('/', '-');
				contentNode.addChild("a", "id", "failedDownload-badtype-"+atype);
				FilterMIMEType typeHandler = ContentFilter.getMIMEType(type);
				HTMLNode failedContent = pageMaker.getInfobox("failed_requests", l10n("failedDBadMIME", new String[]{ "size", "type" }, new String[]{ String.valueOf(getters.size()), type }), contentNode, "download-failed-"+atype, false);
				// FIXME add a class for easier styling.
				KnownUnsafeContentTypeException e = new KnownUnsafeContentTypeException(typeHandler);
				failedContent.addChild("p", l10n("badMIMETypeIntro", "type", type));
				List<String> detail = e.details();
				if(detail != null && !detail.isEmpty()) {
					HTMLNode list = failedContent.addChild("ul");
					for(String s : detail)
						list.addChild("li", s);
				}
				failedContent.addChild("p", l10n("mimeProblemFetchAnyway"));
				Collections.sort(getters, jobComparator);
				if (advancedModeEnabled) {
					failedContent.addChild(createRequestTable(pageMaker, ctx, getters, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.PERSISTENCE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-download-file-badmime", type, QueueType.FailedBadMIMEType));
				} else {
					failedContent.addChild(createRequestTable(pageMaker, ctx, getters, new QueueColumn[] { QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-download-file-badmime", type, QueueType.FailedBadMIMEType));
				}
			}
		}

		if(!failedUnknownMIMEType.isEmpty()) {
			String[] types = failedUnknownMIMEType.keySet().toArray(new String[failedUnknownMIMEType.size()]);
			Arrays.sort(types);
			for(String type : types) {
				LinkedList<DownloadRequestStatus> getters = failedUnknownMIMEType.get(type);
				String atype = type.replace("-", "--").replace('/', '-');
				contentNode.addChild("a", "id", "failedDownload-unknowntype-"+atype);
				HTMLNode failedContent = pageMaker.getInfobox("failed_requests", l10n("failedDUnknownMIME", new String[]{ "size", "type" }, new String[]{ String.valueOf(getters.size()), type }), contentNode, "download-failed-"+atype, false);
				// FIXME add a class for easier styling.
				failedContent.addChild("p", NodeL10n.getBase().getString("UnknownContentTypeException.explanation", "type", type));
				failedContent.addChild("p", l10n("mimeProblemFetchAnyway"));
				Collections.sort(getters, jobComparator);
				if (advancedModeEnabled) {
					failedContent.addChild(createRequestTable(pageMaker, ctx, getters, new QueueColumn[] { QueueColumn.IDENTIFIER, QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.PERSISTENCE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-download-file-unknownmime", type, QueueType.FailedUnknownMIMEType));
				} else {
					failedContent.addChild(createRequestTable(pageMaker, ctx, getters, new QueueColumn[] { QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "failed-download-file-unknownmime", type, QueueType.FailedUnknownMIMEType));
				}
			}

		}

		if (!uncompletedDownload.isEmpty()) {
			contentNode.addChild("a", "id", "uncompletedDownload");
			HTMLNode uncompletedContent = pageMaker.getInfobox("requests_in_progress", l10n("wipD", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedDownload.size()) }), contentNode, "download-progressing", false);
			if (advancedModeEnabled) {
		uncompletedContent.addChild(
		    createRequestTable(
			pageMaker, ctx, uncompletedDownload,
			new QueueColumn[] {
			    QueueColumn.IDENTIFIER, QueueColumn.PRIORITY, QueueColumn.SIZE,
			    QueueColumn.MIME_TYPE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY,
			    /* FIXME: This column has been disabled since it will always show
			     * "never" even if parts of the file transfer failed due to temporary
			     * reasons such as "data not found" / "route not found" / etc. This is
			     * due to shortcomings in the underlying event framework. Please
			     * re-enable it once the underlying issue is fixed:
			     * https://bugs.freenetproject.org/view.php?id=6526 */
			    // QueueColumn.LAST_FAILURE,
			    QueueColumn.PERSISTENCE, QueueColumn.FILENAME,
			    QueueColumn.KEY, QueueColumn.COMPAT_MODE },
			priorityClasses, advancedModeEnabled, "uncompleted-download",
			QueueType.UncompletedDownload)
		);
			} else {
				uncompletedContent.addChild(createRequestTable(pageMaker, ctx, uncompletedDownload, new QueueColumn[] { QueueColumn.PRIORITY, QueueColumn.SIZE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "uncompleted-download", QueueType.UncompletedDownload));
			}
		}

		if (!uncompletedUpload.isEmpty()) {
			contentNode.addChild("a", "id", "uncompletedUpload");
			HTMLNode uncompletedContent = pageMaker.getInfobox("requests_in_progress", l10n("wipU", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedUpload.size()) }), contentNode, "upload-progressing", false);
			if (advancedModeEnabled) {
		uncompletedContent.addChild(
		    createRequestTable(
			pageMaker, ctx, uncompletedUpload,
			new QueueColumn[] {
			    QueueColumn.IDENTIFIER, QueueColumn.PRIORITY, QueueColumn.SIZE,
			    QueueColumn.MIME_TYPE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY,
			    /* FIXME: This column has been disabled since it will always show
			     * "never" even if parts of the file transfer failed due to temporary
			     * reasons such as "data not found" / "route not found" / etc. This is
			     * due to shortcomings in the underlying event framework. Please
			     * re-enable it once the underlying issue is fixed:
			     * https://bugs.freenetproject.org/view.php?id=6526 */
			    // QueueColumn.LAST_FAILURE,
			    QueueColumn.PERSISTENCE, QueueColumn.FILENAME,
			    QueueColumn.KEY },
			priorityClasses, advancedModeEnabled, "uncompleted-upload-file",
			QueueType.UncompletedUpload)
		);
			} else {
				uncompletedContent.addChild(createRequestTable(pageMaker, ctx, uncompletedUpload, new QueueColumn[] { QueueColumn.PRIORITY, QueueColumn.FILENAME, QueueColumn.SIZE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "uncompleted-upload-file", QueueType.UncompletedUpload));
			}
		}

		if (!uncompletedDirUpload.isEmpty()) {
			contentNode.addChild("a", "id", "uncompletedDirUpload");
			HTMLNode uncompletedContent = pageMaker.getInfobox("requests_in_progress", l10n("wipDU", new String[]{ "size" }, new String[]{ String.valueOf(uncompletedDirUpload.size()) }), contentNode, "download-progressing upload-progressing", false);
			if (advancedModeEnabled) {
		uncompletedContent.addChild(
		    createRequestTable(
			pageMaker, ctx, uncompletedDirUpload,
			new QueueColumn[] {
			    QueueColumn.IDENTIFIER, QueueColumn.FILES, QueueColumn.PRIORITY,
			    QueueColumn.TOTAL_SIZE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY,
			    /* FIXME: This column has been disabled since it will always show
			     * "never" even if parts of the file transfer failed due to temporary
			     * reasons such as "data not found" / "route not found" / etc. This is
			     * due to shortcomings in the underlying event framework. Please
			     * re-enable it once the underlying issue is fixed:
			     * https://bugs.freenetproject.org/view.php?id=6526 */
			    // QueueColumn.LAST_FAILURE,
			    QueueColumn.PERSISTENCE, QueueColumn.KEY },
			priorityClasses, advancedModeEnabled, "uncompleted-upload-dir",
			QueueType.UncompletedDirUpload)
		);
			} else {
				uncompletedContent.addChild(createRequestTable(pageMaker, ctx, uncompletedDirUpload, new QueueColumn[] { QueueColumn.PRIORITY, QueueColumn.FILES, QueueColumn.TOTAL_SIZE, QueueColumn.PROGRESS, QueueColumn.LAST_ACTIVITY, QueueColumn.KEY }, priorityClasses, advancedModeEnabled, "uncompleted-upload-dir", QueueType.UncompletedDirUpload));
			}
		}

		if(!uploads) {
			contentNode.addChild(createBulkDownloadForm(ctx, pageMaker));
		}

		return pageNode;
	}

	private HTMLNode sendEmptyQueuePage(ToadletContext ctx, PageMaker pageMaker) {
	PageNode page = pageMaker.getPageNode(l10n("title"+(uploads?"Uploads":"Downloads")), ctx);
	HTMLNode pageNode = page.outer;
	HTMLNode contentNode = page.content;
	/* add alert summary box */
	if(ctx.isAllowedFullAccess())
	    contentNode.addChild(ctx.getAlertManager().createSummary());
	HTMLNode infoboxContent = pageMaker.getInfobox("infobox-information", l10n("globalQueueIsEmpty"), contentNode, "queue-empty", true);
	infoboxContent.addChild("#", l10n("noTaskOnGlobalQueue"));
	if(!uploads)
	    contentNode.addChild(createBulkDownloadForm(ctx, pageMaker));
	return pageNode;
    }

    private HTMLNode createReasonCell(String failureReason) {
		HTMLNode reasonCell = new HTMLNode("td", "class", "request-reason");
		if (failureReason == null) {
			reasonCell.addChild("span", "class", "failure_reason_unknown", l10n("unknown"));
		} else {
			reasonCell.addChild("span", "class", "failure_reason_is", failureReason);
		}
		return reasonCell;
	}

	public static HTMLNode createProgressCell(boolean advancedMode, boolean started, COMPRESS_STATE compressing, int fetched, int failed, int fatallyFailed, int min, int total, boolean finalized, boolean upload) {
		HTMLNode progressCell = new HTMLNode("td", "class", "request-progress");
		if (!started) {
			progressCell.addChild("#", l10n("starting"));
			return progressCell;
		}
		if(compressing == COMPRESS_STATE.WAITING && advancedMode) {
			progressCell.addChild("#", l10n("awaitingCompression"));
			return progressCell;
		}
		if(compressing != COMPRESS_STATE.WORKING) {
			progressCell.addChild("#", l10n("compressing"));
			return progressCell;
		}

		//double frac = p.getSuccessFraction();
		if (!advancedMode || total < min /* FIXME why? */) {
			total = min;
		}

		if ((fetched < 0) || (total <= 0)) {
			progressCell.addChild("span", "class", "progress_fraction_unknown", l10n("unknown"));
		} else {
			int fetchedPercent = (int) (fetched / (double) total * 100);
			int failedPercent = (int) (failed / (double) total * 100);
			int fatallyFailedPercent = (int) (fatallyFailed / (double) total * 100);
			int minPercent = (int) (min / (double) total * 100);
			HTMLNode progressBar = progressCell.addChild("div", "class", "progressbar");
			progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-done", "width: " + fetchedPercent + "%;" });

			if (failed > 0)
				progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-failed", "width: " + failedPercent + "%;" });
			if (fatallyFailed > 0)
				progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-failed2", "width: " + fatallyFailedPercent + "%;" });
			if ((fetched + failed + fatallyFailed) < min)
				progressBar.addChild("div", new String[] { "class", "style" }, new String[] { "progressbar-min", "width: " + (minPercent - fetchedPercent) + "%;" });

			NumberFormat nf = NumberFormat.getInstance();
			nf.setMaximumFractionDigits(1);
			String prefix = '('+Integer.toString(fetched) + "/ " + Integer.toString(min)+"): ";
			if (finalized) {
				progressBar.addChild("div", new String[] { "class", "title" }, new String[] { "progress_fraction_finalized", prefix + l10n("progressbarAccurate") }, nf.format((int) ((fetched / (double) min) * 1000) / 10.0) + '%');
			} else {
				String text = nf.format((int) ((fetched / (double) min) * 1000) / 10.0)+ '%';
				if(!finalized)
					text = "" + fetched + " ("+text+"??)";
				progressBar.addChild("div", new String[] { "class", "title" }, new String[] { "progress_fraction_not_finalized", prefix + NodeL10n.getBase().getString(upload ? "QueueToadlet.uploadProgressbarNotAccurate" : "QueueToadlet.progressbarNotAccurate") }, text);
			}
		}
		return progressCell;
	}

	private HTMLNode createNumberCell(int numberOfFiles) {
		HTMLNode numberCell = new HTMLNode("td", "class", "request-files");
		numberCell.addChild("span", "class", "number_of_files", String.valueOf(numberOfFiles));
		return numberCell;
	}

	private HTMLNode createFilenameCell(File filename) {
		HTMLNode filenameCell = new HTMLNode("td", "class", "request-filename");
		if (filename != null) {
			filenameCell.addChild("span", "class", "filename_is", filename.toString());
		} else {
			filenameCell.addChild("span", "class", "filename_none", l10n("none"));
		}
		return filenameCell;
	}

	private HTMLNode createPriorityCell(short priorityClass, String[] priorityClasses) {
		HTMLNode priorityCell = new HTMLNode("td", "class", "request-priority");
		if(priorityClass < 0 || priorityClass >= priorityClasses.length) {
			priorityCell.addChild("span", "class", "priority_unknown", l10n("unknown"));
		} else {
			priorityCell.addChild("span", "class", "priority_is", priorityClasses[priorityClass]);
		}
		return priorityCell;
	}

	private HTMLNode createPriorityControl(PageMaker pageMaker, ToadletContext ctx, short priorityClass, String[] priorityClasses, boolean advancedModeEnabled, boolean isUpload, String controlSuffix) {
		HTMLNode priorityDiv = new HTMLNode("div", "class", "request-priority nowrap");
		priorityDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "change_priority" + controlSuffix, NodeL10n.getBase().getString(isUpload ? "QueueToadlet.changeUploadPriorities" : "QueueToadlet.changeDownloadPriorities") });
		HTMLNode prioritySelect = priorityDiv.addChild("select", "name", "priority"+controlSuffix);
		for (int p = 0; p < RequestStarter.NUMBER_OF_PRIORITY_CLASSES; p++) {
			if(p <= RequestStarter.INTERACTIVE_PRIORITY_CLASS && !advancedModeEnabled) continue;
			if (p == priorityClass) {
				prioritySelect.addChild("option", new String[] { "value", "selected" }, new String[] { String.valueOf(p), "selected" }, priorityClasses[p]);
			} else {
				prioritySelect.addChild("option", "value", String.valueOf(p), priorityClasses[p]);
			}
		}
		return priorityDiv;
	}
	
	private HTMLNode createRecommendControl(PageMaker pageMaker, ToadletContext ctx) {
		HTMLNode recommendDiv = new HTMLNode("div", "class", "request-recommend");
		recommendDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "recommend_request", l10n("recommendFilesToFriends") });
		return recommendDiv;
	}

	/** Create a delete or restart control at the top of a table. It applies to whichever requests are checked in the table below. */
	private HTMLNode createDeleteControl(PageMaker pageMaker, ToadletContext ctx, String mimeType, QueueType queueType) {
		HTMLNode deleteDiv = new HTMLNode("div", "class", "request-delete");
		if (queueType == QueueType.CompletedDownloadToTemp) {
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "delete_request", l10n("deleteFilesFromTemp") });
		} else if (!queueType.isCompleted) {
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "remove_request", l10n("cancelSelected") });
		} else {
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "remove_request", l10n("removeFilesFromList") });
		}
		if (queueType == QueueType.CompletedDownloadToDisk) {
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "remove_finished_downloads_request", l10n("removeFinishedDownloads") });
		}
		if (queueType == QueueType.CompletedUpload) {
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "remove_finished_uploads_request", l10n("removeFinishedUploads") });
		}
		if (queueType.isFailed) {
			String restartName = NodeL10n.getBase().getString("QueueToadlet.restartSelected");
			deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "restart_request", restartName });
			if(mimeType != null) {
				HTMLNode input = deleteDiv.addChild("input", new String[] { "type", "name", "value" }, new String[] {"checkbox", "disableFilterData", "disableFilterData" });
				deleteDiv.addChild("#", l10n("disableFilter", "type", mimeType));
			}
		}
		return deleteDiv;
	}

	private HTMLNode createPanicBox(PageMaker pageMaker, ToadletContext ctx) {
		InfoboxNode infobox = pageMaker.getInfobox("infobox-alert", l10n("panicButtonTitle"), "panic-button", true);
		HTMLNode panicBox = infobox.outer;
		HTMLNode panicForm = ctx.addFormChild(infobox.content, path(), "queuePanicForm");
		panicForm.addChild("#", (SimpleToadletServer.noConfirmPanic ? l10n("panicButtonNoConfirmation") : l10n("panicButtonWithConfirmation")) + ' ');
		panicForm.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "panic", l10n("panicButton") });
		return panicBox;
	}

	private HTMLNode createIdentifierCell(FreenetURI uri, String identifier, boolean directory) {
		HTMLNode identifierCell = new HTMLNode("td", "class", "request-identifier");
		if (uri != null) {
			identifierCell.addChild("span", "class", "identifier_with_uri").addChild("a", "href", "/" + uri + (directory ? "/" : ""), identifier);
		} else {
			identifierCell.addChild("span", "class", "identifier_without_uri", identifier);
		}
		return identifierCell;
	}

	private HTMLNode createPersistenceCell(boolean persistent, boolean persistentForever) {
		HTMLNode persistenceCell = new HTMLNode("td", "class", "request-persistence");
		if (persistentForever) {
			persistenceCell.addChild("span", "class", "persistence_forever", l10n("persistenceForever"));
		} else if (persistent) {
			persistenceCell.addChild("span", "class", "persistence_reboot", l10n("persistenceReboot"));
		} else {
			persistenceCell.addChild("span", "class", "persistence_none", l10n("persistenceNone"));
		}
		return persistenceCell;
	}

	private HTMLNode createTypeCell(String type) {
		HTMLNode typeCell = new HTMLNode("td", "class", "request-type");
		if (type != null) {
			typeCell.addChild("span", "class", "mimetype_is", type);
		} else {
			typeCell.addChild("span", "class", "mimetype_unknown", l10n("unknown"));
		}
		return typeCell;
	}

	private HTMLNode createSizeCell(long dataSize, boolean confirmed, boolean advancedModeEnabled) {
		HTMLNode sizeCell = new HTMLNode("td", "class", "request-size");
		if (dataSize > 0 && (confirmed || advancedModeEnabled)) {
			sizeCell.addChild("span", "class", "filesize_is", (confirmed ? "" : ">= ") + SizeUtil.formatSize(dataSize) + (confirmed ? "" : " ??"));
		} else {
			sizeCell.addChild("span", "class", "filesize_unknown", l10n("unknown"));
		}
		return sizeCell;
	}

	private HTMLNode createKeyCell(FreenetURI uri, boolean addSlash) {
		HTMLNode keyCell = new HTMLNode("td", "class", "request-key");
		if (uri != null) {
			keyCell.addChild("span", "class", "key_is").addChild("a", "href", '/' + uri.toString() + (addSlash ? "/" : ""), uri.toShortString() + (addSlash ? "/" : ""));
		} else {
			keyCell.addChild("span", "class", "key_unknown", l10n("unknown"));
		}
		return keyCell;
	}

	private HTMLNode createBulkDownloadForm(ToadletContext ctx, PageMaker pageMaker) {
		InfoboxNode infobox = pageMaker.getInfobox(
			l10n("downloadFiles"), "grouped-downloads", true);
		HTMLNode downloadBox = infobox.outer;
		HTMLNode downloadBoxContent = infobox.content;
		HTMLNode downloadForm = ctx.addFormChild(downloadBoxContent, path(), "queueDownloadForm");
		downloadForm.addChild("#", l10n("downloadFilesInstructions"));
		downloadForm.addChild("br");
		downloadForm.addChild("textarea",
			new String[] { "id", "name", "cols", "rows" },
			new String[] { "bulkDownloads", "bulkDownloads", "120", "8" });
		downloadForm.addChild("br");
		PHYSICAL_THREAT_LEVEL threatLevel = core.getNode().getSecurityLevels().getPhysicalThreatLevel();
		//Force downloading to encrypted space if high/maximum threat level or if the user has disabled
		//downloading to disk.
		if(threatLevel == PHYSICAL_THREAT_LEVEL.HIGH || threatLevel == PHYSICAL_THREAT_LEVEL.MAXIMUM ||
			core.isDownloadDisabled()) {
			downloadForm.addChild("input",
				new String[] { "type", "name", "value" },
				new String[] { "hidden", "target", "direct" });
		} else if(threatLevel == PHYSICAL_THREAT_LEVEL.LOW) {
			downloadForm.addChild("input",
				new String[] { "type", "name", "value" },
				new String[] { "hidden", "target", "disk" });
			selectLocation(downloadForm);
		} else {
			downloadForm.addChild("br");
			downloadForm.addChild("input",
				new String[] { "type", "value", "name", "id" },
				new String[] { "radio", "disk", "target", "bulkDownloadSelectOptionDisk" }
					//Nicer spacing for radio button
				).addChild("label",
					new String[] { "for" },
					new String[] { "bulkDownloadSelectOptionDisk" },
					' '+l10n("bulkDownloadSelectOptionDisk")+' ');
			selectLocation(downloadForm);
			downloadForm.addChild("br");
			downloadForm.addChild("input",
				new String[] { "type", "value", "name", "checked", "id" },
				new String[] { "radio", "direct", "target", "checked", "bulkDownloadSelectOptionDirect" }
				).addChild("label",
					new String[] { "for" },
					new String[] { "bulkDownloadSelectOptionDirect" },
					' '+l10n("bulkDownloadSelectOptionDirect")+' ');
		}
		HTMLNode filterControl = downloadForm.addChild("div", l10n("filterData"));
		filterControl.addChild("input",
			new String[] { "type", "name", "value", "checked", "id" },
			new String[] { "checkbox", "filterData", "filterData", "checked", "filterDataMessage"});
		filterControl.addChild("label",
			new String[] { "for" },
			new String[] { "filterDataMessage" },
			l10n("filterDataMessage"));
		downloadForm.addChild("br");
		downloadForm.addChild("input",
			new String[] { "type", "name", "value" },
			new String[] { "submit", "insert", l10n("download") });
		return downloadBox;
	}

	private void selectLocation(HTMLNode node) {
		String downloadLocation = core.getDownloadsDir().getAbsolutePath();
		//If the download directory isn't allowed, yet downloading is, at least one directory must
		//have been explicitly defined, so take the first one.
		if (!core.allowDownloadTo(core.getDownloadsDir())) {
			downloadLocation = core.getAllowedDownloadDirs()[0].getAbsolutePath();
		}
		node.addChild("input",
			new String[] { "type", "name", "value", "maxlength", "size" },
			new String[] { "text", "path", downloadLocation, Integer.toString(MAX_FILENAME_LENGTH),
				String.valueOf(downloadLocation.length())});
		node.addChild("input",
			new String[] { "type", "name", "value" },
			new String[] { "submit", "select-location", l10n("browseToChange")+"..." });
	}

	/**
	 * Creates a table cell that contains the time of the last activity, as per
	 * {@link TimeUtil#formatTime(long)}.
	 *
	 * @param now
	 *	      The current time (for a unified point of reference for the
	 *	      whole page)
	 * @param lastActivity
	 *	      The last activity of the request
	 * @return The created table cell HTML node
	 */
	private HTMLNode createLastActivityCell(long now, Date lastActivity) {
		HTMLNode lastActivityCell = new HTMLNode("td", "class", "request-last-activity");
		if (lastActivity == null) {
	    // During normal operation, lastActivity will never be null even if there was no
	    // activity yet. It will default to the Date when the request was added. (See
	    // ClientRequester.getLatestSuccess() for the usability motivation behind that.)
	    // lastActivity can however be null if the user had been using a pre-release of
	    // purge-db4o which did not store the lastActivity Date to the database yet.
	    // Thus, we initialize to "unknown" instead of "never" to stress that there was possibly
	    // activity but we cannot know because the Date was not stored yet.
			lastActivityCell.addChild("i", l10n("lastActivity.unknown"));
		} else {
	    lastActivityCell.addChild("#", l10n("lastActivity.ago", "time",
		TimeUtil.formatTime(now - lastActivity.getTime())));
		}
		return lastActivityCell;
	}
	
    /** @see #createLastActivityCell(long, Date) */
    private HTMLNode createLastFailureCell(long now, Date lastFailure) {
	HTMLNode lastFailureCell = new HTMLNode("td", "class", "request-last-failure");
	if (lastFailure == null) {
	    // This is "never" instead of "unknown" because the backend of RequestStatus uses null
	    // to signalize that no failure has happened yet.
	    lastFailureCell.addChild("i", l10n("lastFailure.never"));
	} else {
	    lastFailureCell.addChild("#", l10n("lastFailure.ago", "time",
		TimeUtil.formatTime(now - lastFailure.getTime())));
	}
	return lastFailureCell;
    }

	private HTMLNode createRequestTable(PageMaker pageMaker, ToadletContext ctx, List<? extends RequestStatus> requests, QueueColumn[] columns, String[] priorityClasses, boolean advancedModeEnabled, String id, QueueType queueType) {
		return createRequestTable(pageMaker, ctx, requests, columns, priorityClasses, advancedModeEnabled, id, null, queueType);
	}
	
	private HTMLNode createRequestTable(PageMaker pageMaker, ToadletContext ctx, List<? extends RequestStatus> requests, QueueColumn[] columns, String[] priorityClasses, boolean advancedModeEnabled, String id, String mimeType, QueueType queueType) {
		boolean hasFriends = core.getNode().getDarknetConnections().length > 0;
		long now = System.currentTimeMillis();
		
		HTMLNode formDiv = new HTMLNode("div", "class", "request-table-form");
		HTMLNode form = ctx.addFormChild(formDiv, path(), "request-table-form-"+id+(advancedModeEnabled?"-advanced":"-simple"));
		
		createRequestTableButtons(form, pageMaker, ctx, mimeType, hasFriends, advancedModeEnabled, priorityClasses, true, queueType);

		HTMLNode table = form.addChild("table", "class", "requests");
		HTMLNode headerRow = table.addChild("tr", "class", "table-header");

		// Checkbox header
		headerRow.addChild("th"); // No description

		//Add a header for each column.
		for (QueueColumn column : columns) {
			switch (column) {
				case IDENTIFIER:
					headerRow.addChild("th").addChild("a", "href", (isReversed ? "?sortBy=id" : "?sortBy=id&reversed")).addChild("#", l10n("identifier"));
					break;
				case SIZE:
					headerRow.addChild("th").addChild("a", "href", (isReversed ? "?sortBy=size" : "?sortBy=size&reversed")).addChild("#", l10n("size"));
					break;
				case MIME_TYPE:
					headerRow.addChild("th", l10n("mimeType"));
					break;
				case PERSISTENCE:
					headerRow.addChild("th", l10n("persistence"));
					break;
				case KEY:
					headerRow.addChild("th", l10n("key"));
					break;
				case FILENAME:
					headerRow.addChild("th", l10n("fileName"));
					break;
				case PRIORITY:
					headerRow.addChild("th", l10n("priority"));
					break;
				case FILES:
					headerRow.addChild("th", l10n("files"));
					break;
				case TOTAL_SIZE:
					headerRow.addChild("th", l10n("totalSize"));
					break;
				case PROGRESS:
					headerRow.addChild("th").addChild("a", "href", (isReversed ? "?sortBy=progress" : "?sortBy=progress&reversed")).addChild("#", l10n("progress"));
					break;
				case REASON:
					headerRow.addChild("th", l10n("reason"));
					break;
				case LAST_ACTIVITY:
					headerRow.addChild("th").addChild("a", "href", (isReversed ? "?sortBy=lastActivity" : "?sortBy=lastActivity&reversed"), l10n("lastActivity"));
					break;
		case LAST_FAILURE:
		    headerRow.addChild("th").addChild("a", "href",
			    (isReversed ? "?sortBy=lastFailure" : "?sortBy=lastFailure&reversed"),
			    l10n("lastFailure"));
		    break;
				case COMPAT_MODE:
					headerRow.addChild("th", l10n("compatibilityMode"));
					break;
			}
		}
		//Add a row with a checkbox for each request.
		int x = 0;
		for (RequestStatus clientRequest : requests) {
			HTMLNode requestRow = table.addChild("tr", "class", "priority" + clientRequest.getPriority());
			requestRow.addChild(createCheckboxCell(clientRequest, x++));

			for (QueueColumn column : columns) {
				switch (column) {
					case IDENTIFIER:
						requestRow.addChild(createIdentifierCell(clientRequest.getURI(), clientRequest.getIdentifier(), clientRequest instanceof UploadDirRequestStatus));
						break;
					case SIZE:
						boolean isFinal = true;
						if(clientRequest instanceof DownloadRequestStatus)
							isFinal = ((DownloadRequestStatus)clientRequest).isTotalFinalized();
						requestRow.addChild(createSizeCell(clientRequest.getDataSize(), isFinal, advancedModeEnabled));
						break;
					case MIME_TYPE:
						if (clientRequest instanceof DownloadRequestStatus) {
							requestRow.addChild(createTypeCell(((DownloadRequestStatus) clientRequest).getMIMEType()));
						} else if (clientRequest instanceof UploadFileRequestStatus) {
							requestRow.addChild(createTypeCell(((UploadFileRequestStatus) clientRequest).getMIMEType()));
						}
						break;
					case PERSISTENCE:
						requestRow.addChild(createPersistenceCell(clientRequest.isPersistent(), clientRequest.isPersistentForever()));
						break;
					case KEY:
						if (clientRequest instanceof DownloadRequestStatus) {
							requestRow.addChild(createKeyCell(((DownloadRequestStatus) clientRequest).getURI(), false));
						} else if (clientRequest instanceof UploadFileRequestStatus) {
							requestRow.addChild(createKeyCell(((UploadFileRequestStatus) clientRequest).getFinalURI(), false));
						}else {
							requestRow.addChild(createKeyCell(((UploadDirRequestStatus) clientRequest).getFinalURI(), true));
						}
						break;
					case FILENAME:
						if (clientRequest instanceof DownloadRequestStatus) {
							requestRow.addChild(createFilenameCell(((DownloadRequestStatus) clientRequest).getDestFilename()));
						} else if (clientRequest instanceof UploadFileRequestStatus) {
							requestRow.addChild(createFilenameCell(((UploadFileRequestStatus) clientRequest).getOrigFilename()));
						}
						break;
					case PRIORITY:
						requestRow.addChild(createPriorityCell(clientRequest.getPriority(), priorityClasses));
						break;
					case FILES:
						requestRow.addChild(createNumberCell(((UploadDirRequestStatus) clientRequest).getNumberOfFiles()));
						break;
					case TOTAL_SIZE:
						requestRow.addChild(createSizeCell(((UploadDirRequestStatus) clientRequest).getTotalDataSize(), true, advancedModeEnabled));
						break;
					case PROGRESS:
						if(clientRequest instanceof UploadFileRequestStatus)
							requestRow.addChild(createProgressCell(ctx.isAdvancedModeEnabled(),
									clientRequest.isStarted(), ((UploadFileRequestStatus)clientRequest).isCompressing(),
									clientRequest.getFetchedBlocks(), clientRequest.getFailedBlocks(),
									clientRequest.getFatalyFailedBlocks(), clientRequest.getMinBlocks(),
									clientRequest.getTotalBlocks(),
									clientRequest.isTotalFinalized() || clientRequest instanceof UploadFileRequestStatus,
									queueType.isUpload));
						else
							requestRow.addChild(createProgressCell(ctx.isAdvancedModeEnabled(),
									clientRequest.isStarted(), COMPRESS_STATE.WORKING,
									clientRequest.getFetchedBlocks(), clientRequest.getFailedBlocks(),
									clientRequest.getFatalyFailedBlocks(), clientRequest.getMinBlocks(),
									clientRequest.getTotalBlocks(),
									clientRequest.isTotalFinalized() || clientRequest instanceof UploadFileRequestStatus,
									queueType.isUpload));
						break;
					case REASON:
						requestRow.addChild(createReasonCell(clientRequest.getFailureReason(false)));
						break;
					case LAST_ACTIVITY:
						requestRow.addChild(createLastActivityCell(now, clientRequest.getLastSuccess()));
						break;
		    case LAST_FAILURE:
			requestRow.addChild(createLastFailureCell(now,
				clientRequest.getLastFailure()));
			break;
					case COMPAT_MODE:
						if(clientRequest instanceof DownloadRequestStatus) {
							requestRow.addChild(createCompatModeCell((DownloadRequestStatus)clientRequest));
						} else {
							requestRow.addChild("td");
						}
						break;
				}
			}
		}
		createRequestTableButtons(form, pageMaker, ctx, mimeType, hasFriends, advancedModeEnabled, priorityClasses, false, queueType);
		return formDiv;
	}

	private boolean queueCannotRecommend(QueueType queueType) {
		return queueType.isUpload && !queueType.isCompleted;
	}
  
	private void createRequestTableButtons(HTMLNode form, PageMaker pageMaker,
			ToadletContext ctx, String mimeType, boolean hasFriends,
			boolean advancedModeEnabled, String[] priorityClasses,	boolean top,
			QueueType queueType) {
		form.addChild(createDeleteControl(pageMaker, ctx, mimeType, queueType));
		if (hasFriends && !queueCannotRecommend(queueType)) {
			form.addChild(createRecommendControl(pageMaker, ctx));
		}
		if (!(queueType.isFailed || queueType.isCompleted)) {
			form.addChild(createPriorityControl(pageMaker, ctx, RequestStarter.BULK_SPLITFILE_PRIORITY_CLASS, priorityClasses, advancedModeEnabled, queueType.isUpload, top ? "_top" : "_bottom"));
		}
	}

	private HTMLNode createCheckboxCell(RequestStatus clientRequest, int counter) {
		HTMLNode cell = new HTMLNode("td", "class", "checkbox-cell");
		String identifier = clientRequest.getIdentifier();
		cell.addChild("input", new String[] { "type", "name", "value" },
				new String[] { "checkbox", "identifier-"+counter, identifier } );
		FreenetURI uri;
		long size = -1;
		String filename = null;
		if(clientRequest instanceof DownloadRequestStatus) {
			uri = clientRequest.getURI();
			size = clientRequest.getDataSize();
		} else if(clientRequest instanceof UploadRequestStatus) {
			uri = ((UploadRequestStatus)clientRequest).getFinalURI();
			size = clientRequest.getDataSize();
		} else {
			uri = null;
		}
		if(uri != null) {
			cell.addChild("input", new String[] { "type", "name", "value" },
					new String[] { "hidden", "key-"+counter, uri.toASCIIString() });
		}
		filename = clientRequest.getPreferredFilenameSafe();
		if(size != -1)
			cell.addChild("input", new String[] { "type", "name", "value" },
					new String[] { "hidden", "size-"+counter, Long.toString(size) });
		if(filename != null)
			cell.addChild("input", new String[] { "type", "name", "value" },
					new String[] { "hidden", "filename-"+counter, filename });
		return cell;
	}

	private HTMLNode createCompatModeCell(DownloadRequestStatus get) {
		HTMLNode compatCell = new HTMLNode("td", "class", "request-compat-mode");
		InsertContext.CompatibilityMode[] compat = get.getCompatibilityMode();
		if(!(compat[0] == InsertContext.CompatibilityMode.COMPAT_UNKNOWN && compat[1] == InsertContext.CompatibilityMode.COMPAT_UNKNOWN)) {
			if(compat[0] == compat[1])
				compatCell.addChild("#", NodeL10n.getBase().getString("InsertContext.CompatibilityMode."+compat[0].name())); // FIXME l10n
			else
				compatCell.addChild("#", NodeL10n.getBase().getString("InsertContext.CompatibilityMode."+compat[0].name())+" - "+NodeL10n.getBase().getString("InsertContext.CompatibilityMode."+compat[1].name())); // FIXME l10n
			byte[] overrideCryptoKey = get.getOverriddenSplitfileCryptoKey();
			if(overrideCryptoKey != null)
				compatCell.addChild("#", " - "+l10n("overriddenCryptoKeyInCompatCell")+": "+HexUtil.bytesToHex(overrideCryptoKey));
			if(get.detectedDontCompress())
				compatCell.addChild("#", " ("+l10n("dontCompressInCompatCell")+")");
		}
		return compatCell;
	}

	/**
	 * List of completed request identifiers which the user hasn't acknowledged yet.
	 */
	private final HashSet<String> completedRequestIdentifiers = new HashSet<String>();

	private final Map<String, GetCompletedEvent> completedGets = new LinkedHashMap<String, GetCompletedEvent>();
	private final Map<String, PutCompletedEvent> completedPuts = new LinkedHashMap<String, PutCompletedEvent>();
	private final Map<String, PutDirCompletedEvent> completedPutDirs = new LinkedHashMap<String, PutDirCompletedEvent>();

	@Override
	public void notifyFailure(ClientRequest req) {
		// FIXME do something???
	}

	@Override
	public void notifySuccess(ClientRequest req) {
		if(uploads == req instanceof ClientGet) return;
		synchronized(completedRequestIdentifiers) {
			completedRequestIdentifiers.add(req.getIdentifier());
		}
		registerAlert(req); // should be safe here
		saveCompletedIdentifiersOffThread();
	}

	private void saveCompletedIdentifiersOffThread() {
		core.getExecutor().execute(new Runnable() {
			@Override
			public void run() {
				saveCompletedIdentifiers();
			}
		}, "Save completed identifiers");
	}

	private void loadCompletedIdentifiers() throws PersistenceDisabledException {
		String dl = uploads ? "uploads" : "downloads";
		File completedIdentifiersList = core.getNode().userDir().file("completed.list."+dl);
		File completedIdentifiersListNew = core.getNode().userDir().file("completed.list."+dl+".bak");
		File oldCompletedIdentifiersList = core.getNode().userDir().file("completed.list");
		boolean migrated = false;
		if(!readCompletedIdentifiers(completedIdentifiersList)) {
			if(!readCompletedIdentifiers(completedIdentifiersListNew)) {
				readCompletedIdentifiers(oldCompletedIdentifiersList);
				migrated = true;
			}
		} else
			oldCompletedIdentifiersList.delete();
		final boolean writeAnyway = migrated;
		core.getClientContext().jobRunner.queue(new PersistentJob() {

			@Override
			public String toString() {
				return "QueueToadlet LoadCompletedIdentifiers";
			}

			@Override
			public boolean run(ClientContext context) {
				String[] identifiers;
				boolean changed = writeAnyway;
				synchronized(completedRequestIdentifiers) {
					identifiers = completedRequestIdentifiers.toArray(new String[completedRequestIdentifiers.size()]);
				}
				for(String identifier: identifiers) {
					ClientRequest req = fcp.getGlobalRequest(identifier);
					if(req == null || req instanceof ClientGet == uploads) {
						synchronized(completedRequestIdentifiers) {
							completedRequestIdentifiers.remove(identifier);
						}
						changed = true;
						continue;
					}
					registerAlert(req);
				}
				if(changed) saveCompletedIdentifiers();
				return false;
			}

		}, NativeThread.HIGH_PRIORITY);
	}

	private boolean readCompletedIdentifiers(File file) {
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			BufferedInputStream bis = new BufferedInputStream(fis);
			InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8);
			BufferedReader br = new BufferedReader(isr);
			synchronized(completedRequestIdentifiers) {
				completedRequestIdentifiers.clear();
				while(true) {
					String identifier = br.readLine();
					if(identifier == null) return true;
					completedRequestIdentifiers.add(identifier);
				}
			}
		} catch (EOFException e) {
			// Normal
			return true;
		} catch (FileNotFoundException e) {
			// Normal
			return false;
		} catch (IOException e) {
			Logger.error(this, "Could not read completed identifiers list from "+file);
			return false;
		} finally {
			Closer.close(fis);
		}
	}

	private void saveCompletedIdentifiers() {
		FileOutputStream fos = null;
		BufferedWriter bw = null;
		String dl = uploads ? "uploads" : "downloads";
		File completedIdentifiersList = core.getNode().userDir().file("completed.list."+dl);
		File completedIdentifiersListNew = core.getNode().userDir().file("completed.list."+dl+".bak");
		File temp;
		try {
			temp = File.createTempFile("completed.list", ".tmp", core.getNode().getUserDir());
			temp.deleteOnExit();
			fos = new FileOutputStream(temp);
			OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
			bw = new BufferedWriter(osw);
			String[] identifiers;
			synchronized(completedRequestIdentifiers) {
				identifiers = completedRequestIdentifiers.toArray(new String[completedRequestIdentifiers.size()]);
			}
			for(String identifier: identifiers)
				bw.write(identifier+'\n');
		} catch (FileNotFoundException e) {
			Logger.error(this, "Unable to save completed requests list (can't find node directory?!!?): "+e, e);
			return;
		} catch (IOException e) {
			Logger.error(this, "Unable to save completed requests list: "+e, e);
			return;
		} finally {
			if(bw != null) {
				try {
					bw.close();
				} catch (IOException e) {
					try {
						fos.close();
					} catch (IOException e1) {
						// Ignore
					}
				}
			} else {
				try {
					fos.close();
				} catch (IOException e1) {
					// Ignore
				}
			}
		}
		completedIdentifiersListNew.delete();
		temp.renameTo(completedIdentifiersListNew);
		if(!completedIdentifiersListNew.renameTo(completedIdentifiersList)) {
			completedIdentifiersList.delete();
			if(!completedIdentifiersListNew.renameTo(completedIdentifiersList)) {
				Logger.error(this, "Unable to store completed identifiers list because unable to rename "+completedIdentifiersListNew+" to "+completedIdentifiersList);
			}
		}
	}

	private void registerAlert(ClientRequest req) {
		final String identifier = req.getIdentifier();
		if(logMINOR)
			Logger.minor(this, "Registering alert for "+identifier);
		if(!req.hasFinished()) {
			if(logMINOR)
				Logger.minor(this, "Request hasn't finished: "+req+" for "+identifier, new Exception("debug"));
			return;
		}
		if(req instanceof ClientGet) {
			FreenetURI uri = ((ClientGet)req).getURI();
			if(uri == null) {
				Logger.error(this, "No URI for supposedly finished request "+req);
				return;
			}
			long size = ((ClientGet)req).getDataSize();
			GetCompletedEvent event = new GetCompletedEvent(identifier, uri, size);
			synchronized(completedGets) {
				completedGets.put(identifier, event);
			}
			core.getAlerts().register(event);
		} else if(req instanceof ClientPut) {
			FreenetURI uri = ((ClientPut)req).getFinalURI();
			if(uri == null) {
				Logger.error(this, "No URI for supposedly finished request "+req);
				return;
			}
			long size = ((ClientPut)req).getDataSize();
			PutCompletedEvent event = new PutCompletedEvent(identifier, uri, size);
			synchronized(completedPuts) {
				completedPuts.put(identifier, event);
			}
			core.getAlerts().register(event);
		} else if(req instanceof ClientPutDir) {
			FreenetURI uri = ((ClientPutDir)req).getFinalURI();
			if(uri == null) {
				Logger.error(this, "No URI for supposedly finished request "+req);
				return;
			}
			long size = ((ClientPutDir)req).getTotalDataSize();
			int files = ((ClientPutDir)req).getNumberOfFiles();
			PutDirCompletedEvent event = new PutDirCompletedEvent(identifier, uri, size, files);
			synchronized(completedPutDirs) {
				completedPutDirs.put(identifier, event);
			}
			core.getAlerts().register(event);
		}
	}

	static String l10n(String key) {
		return NodeL10n.getBase().getString("QueueToadlet."+key);
	}

	static String l10n(String key, String pattern, String value) {
		return NodeL10n.getBase().getString("QueueToadlet."+key, pattern, value);
	}

	static String l10n(String key, String[] pattern, String[] value) {
		return NodeL10n.getBase().getString("QueueToadlet."+key, pattern, value);
	}

	@Override
	public void onRemove(ClientRequest req) {
		String identifier = req.getIdentifier();
		synchronized(completedRequestIdentifiers) {
			completedRequestIdentifiers.remove(identifier);
		}
		if(req instanceof ClientGet)
			synchronized(completedGets) {
				completedGets.remove(identifier);
			}
		else if(req instanceof ClientPut)
			synchronized(completedPuts) {
				completedPuts.remove(identifier);
			}
		else if(req instanceof ClientPutDir)
			synchronized(completedPutDirs) {
				completedPutDirs.remove(identifier);
			}
		saveCompletedIdentifiersOffThread();
	}

	@Override
	public boolean isEnabled(ToadletContext ctx) {
		return (!container.publicGatewayMode()) || ((ctx != null) && ctx.isAllowedFullAccess());
	}

	static final String PATH_UPLOADS = "/uploads/";
	static final String PATH_DOWNLOADS = "/downloads/";
	
	static final HTMLNode DOWNLOADS_LINK = 
		HTMLNode.link(PATH_DOWNLOADS).setReadOnly();
	static final HTMLNode UPLOADS_LINK =
		HTMLNode.link(PATH_UPLOADS).setReadOnly();

	@Override
	public String path() {
		if(uploads)
			return PATH_UPLOADS;
		else
			return PATH_DOWNLOADS;
	}

	private class GetCompletedEvent extends StoringUserEvent<GetCompletedEvent> {

		private final String identifier;
		private final FreenetURI uri;
		private final long size;

		public GetCompletedEvent(String identifier, FreenetURI uri, long size) {
			super(Type.GetCompleted, true, null, null, null, null, UserAlert.MINOR, true, NodeL10n.getBase().getString("UserAlert.hide"), true, null, completedGets);
			this.identifier = identifier;
			this.uri = uri;
			this.size = size;
		}

		@Override
		public void onDismiss() {
			super.onDismiss();
			saveCompletedIdentifiersOffThread();
		}

		@Override
		public void onEventDismiss() {
			synchronized(completedRequestIdentifiers) {
				completedRequestIdentifiers.remove(identifier);
			}
		}

		@Override
		public HTMLNode getEventHTMLText() {
			HTMLNode text = new HTMLNode("div");
			NodeL10n.getBase().addL10nSubstitution(text, "QueueToadlet.downloadSucceeded",
					new String[] { "link", "origlink", "filename", "size" },
					new HTMLNode[] { HTMLNode.link("/"+uri.toASCIIString()+"?max-size="+size), HTMLNode.link("/"+uri.toASCIIString()), HTMLNode.text(uri.getPreferredFilename()), HTMLNode.text(SizeUtil.formatSize(size))});
			return text;
		}

		@Override
		public String getTitle() {
			String title = null;
			synchronized(events) {
				if(events.size() == 1)
					title = l10n("downloadSucceededTitle", "filename", uri.getPreferredFilename());
				else
					title = l10n("downloadsSucceededTitle", "nr", Integer.toString(events.size()));
			}
			return title;
		}

		@Override
		public String getShortText() {
			return getTitle();
		}

		@Override
		public String getEventText() {
			return l10n("downloadSucceededTitle", "filename", uri.getPreferredFilename());
		}

	}

	private class PutCompletedEvent extends StoringUserEvent<PutCompletedEvent> {

		private final String identifier;
		private final FreenetURI uri;
		private final long size;

		public PutCompletedEvent(String identifier, FreenetURI uri, long size) {
			super(Type.PutCompleted, true, null, null, null, null, UserAlert.MINOR, true, NodeL10n.getBase().getString("UserAlert.hide"), true, null, completedPuts);
			this.identifier = identifier;
			this.uri = uri;
			this.size = size;
		}

		@Override
		public void onDismiss() {
			super.onDismiss();
			saveCompletedIdentifiersOffThread();
		}

		@Override
		public void onEventDismiss() {
			synchronized(completedRequestIdentifiers) {
				completedRequestIdentifiers.remove(identifier);
			}
		}

		@Override
		public HTMLNode getEventHTMLText() {
			HTMLNode text = new HTMLNode("div");
			NodeL10n.getBase().addL10nSubstitution(text, "QueueToadlet.uploadSucceeded",
					new String[] { "link", "filename", "size" },
					new HTMLNode[] { HTMLNode.link("/"+uri.toASCIIString()), HTMLNode.text(uri.getPreferredFilename()), HTMLNode.text(SizeUtil.formatSize(size))});
			return text;
		}

		@Override
		public String getTitle() {
			String title = null;
			synchronized(events) {
				if(events.size() == 1)
					title = l10n("uploadSucceededTitle", "filename", uri.getPreferredFilename());
				else
					title = l10n("uploadsSucceededTitle", "nr", Integer.toString(events.size()));
			}
			return title;
		}

		@Override
		public String getShortText() {
			return getTitle();
		}

		@Override
		public String getEventText() {
			return l10n("uploadSucceededTitle", "filename", uri.getPreferredFilename());
		}

	}

	private class PutDirCompletedEvent extends StoringUserEvent<PutDirCompletedEvent> {

		private final String identifier;
		private final FreenetURI uri;
		private final long size;
		private final int files;

		public PutDirCompletedEvent(String identifier, FreenetURI uri, long size, int files) {
			super(Type.PutDirCompleted, true, null, null, null, null, UserAlert.MINOR, true, NodeL10n.getBase().getString("UserAlert.hide"), true, null, completedPutDirs);
			this.identifier = identifier;
			this.uri = uri;
			this.size = size;
			this.files = files;
		}

		@Override
		public void onDismiss() {
			super.onDismiss();
			saveCompletedIdentifiersOffThread();
		}

		@Override
		public void onEventDismiss() {
			synchronized(completedRequestIdentifiers) {
				completedRequestIdentifiers.remove(identifier);
			}
		}

		@Override
		public HTMLNode getEventHTMLText() {
			String name = uri.getPreferredFilename();
			HTMLNode text = new HTMLNode("div");
			NodeL10n.getBase().addL10nSubstitution(text, "QueueToadlet.siteUploadSucceeded",
					new String[] { "link", "filename", "size", "files" },
					new HTMLNode[] { HTMLNode.link("/"+uri.toASCIIString()), HTMLNode.text(name), HTMLNode.text(SizeUtil.formatSize(size)), HTMLNode.text(files) });
			return text;
		}

		@Override
		public String getTitle() {
			String title = null;
			synchronized(events) {
				if(events.size() == 1)
					title = l10n("siteUploadSucceededTitle", "filename", uri.getPreferredFilename());
				else
					title = l10n("sitesUploadSucceededTitle", "nr", Integer.toString(events.size()));
			}
			return title;
		}

		@Override
		public String getShortText() {
			return getTitle();
		}

		@Override
		public String getEventText() {
			return l10n("siteUploadSucceededTitle", "filename", uri.getPreferredFilename());
		}

	}

}
