Worked around FCP GetCHKOnly bug. Other fixes. Has some local wormarc changes. Go back over this changeset.
diff --git a/alien/src/wormarc/IOUtil.java b/alien/src/wormarc/IOUtil.java
--- a/alien/src/wormarc/IOUtil.java
+++ b/alien/src/wormarc/IOUtil.java
@@ -37,6 +37,7 @@ import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.DigestInputStream;
import java.util.Random;
@@ -83,27 +84,38 @@ public class IOUtil {
}
}
+ public final static DigestInputStream getSha1DigestInputStream(InputStream fromStream)
+ throws IOException {
+ try {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA");
+ return new DigestInputStream(fromStream, sha1);
+ }
+ catch (NoSuchAlgorithmException nsae) {
+ fromStream.close();
+ throw new IOException("Couldn't load SHA1 algorithm.");
+ }
+ }
+
+ private static class NullOutputStream extends OutputStream {
+ public void write(int b) throws IOException {}
+ public void write(byte[] b,
+ int off,
+ int len)
+ throws IOException {
+ if (b == null) throw new NullPointerException();
+ if (off < 0 || len < 0 || off + len > b.length) {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+
+ // Hmmm... old code was probably faster.
// Closes stream.
public final static LinkDigest getFileDigest(InputStream fromStream) throws IOException {
- MessageDigest sha1 = null;
+ DigestInputStream inputStream = getSha1DigestInputStream(fromStream);
try {
- try {
- sha1 = MessageDigest.getInstance("SHA");
- }
- catch (NoSuchAlgorithmException nsae) {
- throw new IOException("Couldn't load SHA1 algorithm.");
- }
-
- // DCI: Better to use a wrapper stream filter from the java crypto lib?
- byte[] buffer = new byte[BUF_LEN];
- while (true) {
- int bytesRead = fromStream.read(buffer);
- if (bytesRead == -1) {
- break;
- }
- sha1.update(buffer, 0, bytesRead);
- }
- return new LinkDigest(sha1.digest());
+ copyAndClose(inputStream, new NullOutputStream());
+ return new LinkDigest(inputStream.getMessageDigest().digest());
}
finally {
fromStream.close();
diff --git a/alien/src/wormarc/io/FCPCommandRunner.java b/alien/src/wormarc/io/FCPCommandRunner.java
--- a/alien/src/wormarc/io/FCPCommandRunner.java
+++ b/alien/src/wormarc/io/FCPCommandRunner.java
@@ -29,6 +29,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
+import java.security.DigestInputStream;
+
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -54,12 +56,13 @@ import net.pterodactylus.fcp.FcpMessage;
import wormarc.Block;
import wormarc.HistoryLinkMap;
import wormarc.IOUtil;
+import wormarc.LinkDigest;
// Christ on a bike! is jfcplib really this complicated?
public class FCPCommandRunner {
private final static Verbosity VERBOSITY = Verbosity.ALL;
private final static Priority PRIORITY = Priority.interactive;
- private final static int MAX_RETRIES = 3;
+ private final static int MAX_RETRIES = 6;
private final static boolean DONT_COMPRESS = true;
private final static String REAL_TIME_FIELD = "RealTimeFlag";
private final static String REAL_TIME_VALUE = "true";
@@ -170,7 +173,9 @@ public class FCPCommandRunner {
}
// DCI: Handle too big! (code == 21). It means the
- handleDone(String.format("ClientGet failed: %d", getFailed.getCode()));
+ handleDone(String.format("ClientGet failed: [%d]: %s",
+ getFailed.getCode(),
+ getFailed.getShortCodeDescription()));
}
public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed) {
@@ -178,7 +183,9 @@ public class FCPCommandRunner {
return;
}
- handleDone(String.format("ClientPut failed: %d", putFailed.getCode()));
+ handleDone(String.format("ClientPut failed: [%d]: %s",
+ putFailed.getCode(),
+ putFailed.getShortCodeDescription()));
}
public void receivedAllData(FcpConnection fcpConnection, AllData allData) {
@@ -248,6 +255,7 @@ public class FCPCommandRunner {
static class GetBlock extends Command {
private long mLength;
private Block mBlock;
+ private String mHexDigest;
private FreenetIO mIO;
protected GetBlock(String name, String uri, long length, FreenetIO io, FCPCommandRunner runner) {
@@ -265,7 +273,11 @@ public class FCPCommandRunner {
if (data == null) {
throw new IllegalArgumentException("data == null");
}
- mBlock = mIO.readLinks(data);
+
+ DigestInputStream digestInput = IOUtil.getSha1DigestInputStream(data);
+ mBlock = mIO.readLinks(digestInput);
+ LinkDigest digest = new LinkDigest(digestInput.getMessageDigest().digest());
+ mHexDigest = digest.toString();
}
protected FcpMessage getStartMessage() {
@@ -279,21 +291,32 @@ public class FCPCommandRunner {
}
public Block getBlock() { return mBlock; }
+ public String getHexDigest() { return mHexDigest; }
}
static class PutBlock extends Command { // DCI: sleazy. How does stream get closed in failure cases?
private long mLength;
- private InputStream mData;
- public PutBlock(String name, long length, InputStream data, FCPCommandRunner runner) {
+ private DigestInputStream mData;
+ private String mHexDigest;
+
+ public PutBlock(String name, long length, InputStream data, FCPCommandRunner runner) throws IOException {
super(name, "CHK@", runner);
mLength = length;
- mData = data;
+ mData = IOUtil.getSha1DigestInputStream(data);
}
protected void handleData(long length, InputStream data) throws IOException {
handleDone("Not expecting AllData");
}
+ public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful) {
+ LinkDigest digest = new LinkDigest(mData.getMessageDigest().digest());
+ mHexDigest = digest.toString();
+
+ // Order important. This posts handleDone.
+ super.receivedPutSuccessful(fcpConnection, putSuccessful);
+ }
+
protected FcpMessage getStartMessage() {
ClientPut msg = new ClientPut(mUri, mFcpId);
msg.setDataLength(mLength);
@@ -305,6 +328,8 @@ public class FCPCommandRunner {
msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
return msg;
}
+ public long getLength() { return mLength; }
+ public String getHexDigest() { return mHexDigest; }
}
static class GetBlockChk extends Command { // DCI: sleazy. How does stream get closed in failure cases?
@@ -330,6 +355,8 @@ public class FCPCommandRunner {
msg.setDontCompress(DONT_COMPRESS);
msg.setPriority(PRIORITY);
msg.setGetCHKOnly(true);
+ msg.setMaxRetries(MAX_RETRIES);
+ msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
return msg;
}
}
@@ -428,6 +455,8 @@ public class FCPCommandRunner {
msg.setDontCompress(DONT_COMPRESS);
msg.setPriority(PRIORITY);
msg.setGetCHKOnly(true); // Also works for SSKs.
+ msg.setField(REAL_TIME_FIELD, REAL_TIME_VALUE);
+ msg.setMaxRetries(MAX_RETRIES);
return msg;
}
}
diff --git a/alien/src/wormarc/io/FreenetIO.java b/alien/src/wormarc/io/FreenetIO.java
--- a/alien/src/wormarc/io/FreenetIO.java
+++ b/alien/src/wormarc/io/FreenetIO.java
@@ -30,8 +30,10 @@ import java.io.PrintStream;
import java.util.Arrays;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import wormarc.Archive;
@@ -48,6 +50,7 @@ import wormarc.RootObjectKind;
public class FreenetIO implements Archive.IO, ArchiveResolver {
private LinkCache mCache;
+ private Map<String, String> mSha1ToChk;
// Transient
private HistoryLinkMap mLinkMap;
@@ -62,16 +65,31 @@ public class FreenetIO implements Archiv
private String mInsertUri;
private String mRequestUri;
+
+ // DCI: REMOVE THIS, NO LONGER USED?
private FreenetTopKey mPreviousTopKey;
private static PrintStream sDebugOut = System.err;
+ protected void debug(String msg) {
+ synchronized(sDebugOut) {
+ sDebugOut.println(msg);
+ }
+ }
+
// Cache can be null.
// When it is non-null all links read from Freenet are dumped to the cache.
- public FreenetIO(String host, int port, LinkCache cache) {
+ // sha1ToChk can be null.
+ // When it is non-null entries are added to the table for all blocks that are read or written.
+ public FreenetIO(String host, int port, LinkCache cache, Map<String, String> sha1ToChk) {
mHost = host;
mPort = port;
mCache = cache;
+ mSha1ToChk = sha1ToChk;
+ }
+
+ public FreenetIO(String host, int port, LinkCache cache) {
+ this(host, port, cache, null);
}
public FreenetIO(String host, int port) {
@@ -171,38 +189,46 @@ public class FreenetIO implements Archiv
mClientName +
IOUtil.randomHexString(12));
- // Precompute the block CHKs so we can skip blocks that
- // are already in Freenet.
+ // Contains full descriptions for blocks that are known
+ // to exist in Freenet.
List<FreenetTopKey.BlockDescription> descriptions =
- precomputeDescriptions(runner, linkMap, blocks);
+ precomputeDescriptions(linkMap, blocks);
if (blocks.size() != descriptions.size()) {
throw new RuntimeException("Assertion Failure: blocks.size() != descriptions.size()");
}
- Set<String> previousChks = new HashSet<String>();
- if (mPreviousTopKey != null) {
- for (FreenetTopKey.BlockDescription desc : mPreviousTopKey.mBlockDescriptions) {
- for (int subIndex = 0; subIndex < desc.mCHKs.size(); subIndex++) {
- previousChks.add(desc.getCHK(subIndex));
- }
- }
- }
-
List<FCPCommandRunner.PutBlock> puts = new ArrayList<FCPCommandRunner.PutBlock>();
for (int index = 0; index < descriptions.size(); index++) {
FreenetTopKey.BlockDescription desc = descriptions.get(index);
- if (previousChks.contains(desc.getCHK(0))) {
- // i.e. the block was already inserted, so skip it.
+ if (desc != null) {
+ // i.e. the block was already inserted, so skip it, but add a place holder.
+ puts.add(null);
continue;
}
+ // Need to insert the block.
puts.add(runner.sendPutBlock(index, linkMap, blocks.get(index)));
}
runner.waitUntilAllFinished();
+ int pos = 0;
for (FCPCommandRunner.PutBlock put : puts) {
- put.raiseOnFailure();
+ if (put != null) {
+ put.raiseOnFailure();
+ List<String> chks = Arrays.asList(put.getUri());
+ descriptions.set(pos,
+ FreenetTopKey.makeDescription(put.getLength(),
+ chks));
+ }
+ pos++;
+ }
+
+ // Hmmm... really should only update the block sha1 -> CHK cache after full success.
+ for (FCPCommandRunner.PutBlock put : puts) {
+ if (put != null) {
+ cacheBlockChk(put.getHexDigest(), put.getUri());
+ }
}
FreenetTopKey topKey = new FreenetTopKey(rootObjects, descriptions);
@@ -214,6 +240,7 @@ public class FreenetIO implements Archiv
runner.waitUntilAllFinished();
putTopKey.raiseOnFailure();
mRequestUri = putTopKey.getUri();
+
} catch (InterruptedException ie) {
throw new IOException("Write timed out.", ie);
} catch (IllegalBase64Exception ibe) {
@@ -281,9 +308,9 @@ public class FreenetIO implements Archiv
List<FCPCommandRunner.GetBlock> gets = new ArrayList<FCPCommandRunner.GetBlock>();
for (FreenetTopKey.BlockDescription desc : topKey.mBlockDescriptions ) {
// LATER: Handle redundant block fetches.
- sDebugOut.println(String.format("Requesting[%d]: %s",
- desc.mLength,
- desc.getCHK(0)));
+ debug(String.format("Requesting[%d]: %s",
+ desc.mLength,
+ desc.getCHK(0)));
gets.add(runner.sendGetBlock(desc.getCHK(0), desc.mLength, count++, this));
}
runner.waitUntilAllFinished();
@@ -293,9 +320,12 @@ public class FreenetIO implements Archiv
for (FCPCommandRunner.GetBlock get : gets) {
get.raiseOnFailure();
blocks.add(get.getBlock());
+
+ // Save, so that we know we don't need to insert this block
+ // when inserting the update.
+ cacheBlockChk(get.getHexDigest(), get.getUri());
}
return new Archive.ArchiveData(blocks, topKey.mRootObjects);
-
} catch (InterruptedException ie) {
throw new IOException("Read timed out.", ie);
} catch (IllegalBase64Exception ibe) {
@@ -304,7 +334,7 @@ public class FreenetIO implements Archiv
mLinkMap = null;
mLinkDataFactory = null;
if (runner != null) {
- sDebugOut.println("FCP Connection -- DISCONNECTING!");
+ debug("FCP Connection -- DISCONNECTING!");
runner.disconnect();
}
}
@@ -332,29 +362,40 @@ public class FreenetIO implements Archiv
}
////////////////////////////////////////////////////////////
- private List<FreenetTopKey.BlockDescription> precomputeDescriptions(FCPCommandRunner runner,
- HistoryLinkMap linkMap,
+ private void cacheBlockChk(String hexDigest, String chk) {
+ synchronized(mSha1ToChk) {
+ debug(String.format("cached: %s -> %s", hexDigest, chk));
+ mSha1ToChk.put(hexDigest, chk);
+ }
+ }
+
+ private String getCachedChk(String hexDigest) {
+ synchronized(mSha1ToChk) {
+ if (hexDigest == null) {
+ return null;
+ }
+ return mSha1ToChk.get(hexDigest);
+ }
+ }
+
+ private List<FreenetTopKey.BlockDescription> precomputeDescriptions(HistoryLinkMap linkMap,
List<Block> blocks)
throws IllegalBase64Exception,
InterruptedException,
IOException {
- // Use the Freenet node to tell us the CHKs for the new blocks without
- // inserting them.
- int count = 0;
- List<FCPCommandRunner.GetBlockChk> getChks = new ArrayList<FCPCommandRunner.GetBlockChk>();
+ List<FreenetTopKey.BlockDescription> descriptions = new ArrayList<FreenetTopKey.BlockDescription>();
for (Block block : blocks) {
- getChks.add(runner.sendGetBlockChk(count++, linkMap, block));
- }
-
- runner.waitUntilAllFinished();
-
- int index = 0;
- List<FreenetTopKey.BlockDescription> descriptions = new ArrayList<FreenetTopKey.BlockDescription>();
- for (FCPCommandRunner.GetBlockChk get : getChks) {
- get.raiseOnFailure();
- descriptions.add(FreenetTopKey.makeDescription(get.getLength(), Arrays.asList(get.getUri())));
- index++;
+ String hexDigest = IOUtil.getFileDigest(linkMap.getBinaryRep(block)).toString();
+ String chk = getCachedChk(hexDigest);
+ if (chk != null) {
+ // Don't need to insert.
+ long length = linkMap.getLength(block); // LATER: do better. Shouldn't be making multiple passes.
+ descriptions.add(FreenetTopKey.makeDescription(length, Arrays.asList(chk)));
+ continue;
+ }
+ // Do need to insert. Add placeholder.
+ descriptions.add(null);
}
return descriptions;
}
@@ -367,7 +408,7 @@ public class FreenetIO implements Archiv
if (fromReference.mKind != ExternalRefs.KIND_FREENET) {
throw new IOException("Reference is not a Freenet URI");
}
- sDebugOut.println("resolving Archive from: " + fromReference.mExternalKey);
+ debug("resolving Archive from: " + fromReference.mExternalKey);
mRequestUri = fromReference.mExternalKey;
Archive loaded = Archive.load(this); // Hmmmm... slurps stuff into the cache. ???
if (!loaded.getRootObject(RootObjectKind.ARCHIVE_MANIFEST).isNullDigest()) {
diff --git a/doc/quickstart.txt b/doc/quickstart.txt
--- a/doc/quickstart.txt
+++ b/doc/quickstart.txt
@@ -11,14 +11,24 @@
# Adjust any other values as necessary. If you're running FMS and Fred on the same machine on the default ports this shouldn't be necessary.
# Click the "Done" button to save the configuration changes.
-=== Finding Other Versions===
+=== Finding Other Versions Of the testwiki===
Click the "Discover" link below to search for other versions of the wiki.
+If you don't see anything read the "FMS configuration" section below.
+
=== Submitting ===
Use the "Submit" link below to submit your changes. It may take a long time for other people to see them.
+=== There are other wikis! ===
+With the default settings you load the "testwiki" I have been using for testing.
+
+There are other wikis out there.
+
+Try entering "freenetdocwiki" in for the "Wiki Name" on the configuration page, then doing "Discover".
+
=== Known Limitations ===
# There is no merging or rebasing (yet).
+# The only way to start an empty wiki from scratch is by loading and unloading the plugin (or running stand alone from the command line).
----
=== Finding Your Private SSK ===
@@ -44,3 +54,16 @@ SSK@YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
<PublishFreesite>false</PublishFreesite>
</Identity>
}}}
+
+----
+===FMS Configuration===
+
+By default the prototype uses the "biss.test000" FMS group.
+Make sure FMS is configured to save messages to this group.
+
+On the FMS Boards maintenance page:
+http://127.0.0.1:18080/boards.htm
+
+Search for "biss.test000" and make sure "Save Received Messages" is checked.
+
+If you can't find the group, add it.
diff --git a/readme.txt b/readme.txt
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
-201102020
+20110227
djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
WARNING:
@@ -73,3 +73,15 @@ KNOWN ISSUES:
o "Cancel" sometimes fails. [WORKAROUND: load and unload the plugin / kill restart the stand alone app.]
o FMS Id displays "???" when importing config with non-default FCP host and/or port.
[WORKAROUND: Click "Done", then click view again and the FMS Id should be correctly displayed.]
+
+---
+Dev notes
+---
+IDEA: shrink blocks by using a token map? use short token in binary rep, fixup to full 20byte hash on read / write?
+IDEA: Support links to other wikis. e.g.: fniki://fms/group/name
+IDEA: Why isn't this file in Creole?
+
+BUG: wikitext should use unix line terminators not DOS (+1 byte per line)
+BUG: No way to create an empty wiki from the UI. [requested by a real user]
+BUG: Default FCP port wrong for CLI client. [requested by a real user]
+
diff --git a/src/fniki/standalone/FnikiContextHandler.java b/src/fniki/standalone/FnikiContextHandler.java
--- a/src/fniki/standalone/FnikiContextHandler.java
+++ b/src/fniki/standalone/FnikiContextHandler.java
@@ -56,14 +56,14 @@ public class FnikiContextHandler impleme
private final String readAsUtf8(HTTPServer.MultipartIterator.Part part) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- while (part.body.available() > 0) { // Do better? Does it matter?
+ while (true) { // Do better? Does it matter?
int oneByte = part.body.read();
if (oneByte == -1) {
- throw new IOException("Unexpected EOF???");
+ break;
}
baos.write(oneByte);
}
-
+ System.err.println("read part bytes: " + baos.toByteArray().length);
return new String(baos.toByteArray(), "utf8");
}
@@ -90,8 +90,10 @@ public class FnikiContextHandler impleme
continue;
}
mParamTable.put(part.name, readAsUtf8(part));
- System.err.println("Set multipart Param: " + part.name + " : " +
- mParamTable.get(part.name));
+ System.err.println(String.format("Set multipart Param: %s[%d]:\n%s",
+ part.name,
+ mParamTable.get(part.name).length(),
+ mParamTable.get(part.name)));
}
mParent.consumeBody();
}
diff --git a/src/fniki/wiki/ArchiveManager.java b/src/fniki/wiki/ArchiveManager.java
--- a/src/fniki/wiki/ArchiveManager.java
+++ b/src/fniki/wiki/ArchiveManager.java
@@ -66,6 +66,12 @@ public class ArchiveManager {
// Base64 SSK public key hash to FMS name. i.e. the part before '@'.
Map<String, String> mNymLut = new HashMap<String, String>();
+ // LATER: Revisit. This was HACK to work around the fact that GetCHKOnly=true
+ // is broken for splitfiles.
+ //
+ // Block hex digest to CHK key map.
+ Map<String, String> mSha1ToChk = new HashMap<String, String>();
+
String mParentUri;
Archive mArchive;
FileManifest mFileManifest;
@@ -89,7 +95,7 @@ public class ArchiveManager {
public String invertPrivateSSK(String value, int timeoutMs) throws IOException {
validatePrivateSSK(value);
- return (new FreenetIO(mFcpHost, mFcpPort)).invertPrivateSSK(value, timeoutMs);
+ return makeIO().invertPrivateSSK(value, timeoutMs);
}
public String getPrivateSSK() { return mPrivateSSK; }
@@ -125,7 +131,7 @@ public class ArchiveManager {
// DCI: Fix this to roll back state on exceptions.
public void load(String uri) throws IOException {
- FreenetIO io = new FreenetIO(mFcpHost, mFcpPort);
+ FreenetIO io = makeIO();
io.setRequestUri(uri);
Archive archive = Archive.load(io);
validateUriHashes(archive, uri, true);
@@ -149,6 +155,10 @@ public class ArchiveManager {
}
}
+ private FreenetIO makeIO() {
+ return new FreenetIO(mFcpHost, mFcpPort, null, mSha1ToChk);
+ }
+
// The name of a jfniki archive includes the hash of the
// full archive manifest file, and hashes of the SSK of
// it's parent(s).
@@ -259,10 +269,8 @@ public class ArchiveManager {
out.println("Insert URI: " + insertUri);
// Push the updated version into Freenet.
- FreenetIO io = new FreenetIO(mFcpHost, mFcpPort);
+ FreenetIO io = makeIO();
io.setInsertUri(insertUri);
- out.println("Trying to read previous top key if possible...");
- io.maybeLoadPreviousTopKey(copy);
out.println("Writing to Freenet...");
copy.write(io);
@@ -300,7 +308,7 @@ public class ArchiveManager {
ExternalRefs.Reference head =
new ExternalRefs.Reference(ExternalRefs.KIND_FREENET, mParentUri);
- FreenetIO freenetResolver = new FreenetIO(mFcpHost, mFcpPort);
+ FreenetIO freenetResolver = makeIO();
Archive archive = freenetResolver.resolve(head);
AuditArchive.getManifestChangeLog(head, archive, freenetResolver, callback);
}
diff --git a/src/fniki/wiki/child/WikiContainer.java b/src/fniki/wiki/child/WikiContainer.java
--- a/src/fniki/wiki/child/WikiContainer.java
+++ b/src/fniki/wiki/child/WikiContainer.java
@@ -43,6 +43,8 @@ import fniki.wiki.ChildContainerExceptio
import fniki.wiki.Query;
import wormarc.FileManifest;
+import wormarc.IOUtil;
+
import fniki.wiki.FreenetWikiTextParser;
import static fniki.wiki.HtmlUtils.*;
import static fniki.wiki.Validations.*;
@@ -226,50 +228,34 @@ public class WikiContainer implements Ch
}
private String getEditorHtml(WikiContext context, String name) throws IOException {
- StringBuilder buffer = new StringBuilder();
- addHeader(name, buffer);
+ String template = null;
+ try {
+ // IMPORTANT: Only multipart/form-data encoding works in plugins.
+ // IMPORTANT: Must be multipart/form-data even for standalone because
+ // the Freenet ContentFilter rewrites the encoding in all forms
+ // to this value.
+ template = IOUtil.readUtf8StringAndClose(SettingConfig.class.getResourceAsStream("/edit_form.html"));
+ } catch (IOException ioe) {
+ return "Couldn't load edit_form.html template from jar???";
+ }
+ String escapedName = escapeHTML(unescapedTitleFromName(name));
String href = makeHref(context.makeLink("/" +name),
"save", null, null, null);
-
-
- buffer.append("<form method=\"post\" action=\"" +
- href +
- "\" enctype=\"");
-
- // IMPORTANT: Only multipart/form-data encoding works in plugins.
- // IMPORTANT: Must be multipart/form-date even for standalone because
- // the Freenet ContentFilter rewrites the encoding in all forms
- // to this value.
- buffer.append("multipart/form-data");
- buffer.append("\" accept-charset=\"UTF-8\">\n");
-
- buffer.append("<input type=hidden name=\"savepage\" value=\"");
- buffer.append(escapeHTML(name));
- buffer.append("\">\n");
-
- buffer.append("<textarea wrap=\"virtual\" name=\"savetext\" rows=\"17\" cols=\"120\">\n");
-
+ String wikiText = "Page doesn't exist in the wiki yet.";
if (context.getStorage().hasPage(name)) {
- buffer.append(escapeHTML(context.getStorage().getPage(name)));
- } else {
- buffer.append(escapeHTML("Page doesn't exist in the wiki yet."));
+ wikiText = context.getStorage().getPage(name);
}
- buffer.append("</textarea>\n");
- buffer.append("<br><input type=submit value=\"Save\">\n");
- buffer.append("<input type=hidden name=formPassword value=\"");
-
- // IMPORTANT: Required by Freenet Plugin.
- buffer.append(context.getString("form_password", "FORM_PASSWORD_NOT_SET")); // Doesn't need escaping.
- buffer.append("\"/>\n");
- buffer.append("<input type=reset value=\"Reset\">\n");
- buffer.append("<br></form>");
-
- buffer.append("<hr>\n");
- buffer.append("</body></html>\n");
-
- return buffer.toString();
+ return String.format(template,
+ escapedName,
+ escapedName,
+ href,
+ escapeHTML(name), // i.e. with '_' chars
+ escapeHTML(wikiText),
+ // IMPORTANT: Required by Freenet Plugin.
+ // Doesn't need escaping.
+ context.getString("form_password", "FORM_PASSWORD_NOT_SET"));
}
public String renderXHTML(WikiContext context, String wikiText) {
diff --git a/templates/edit_form.html b/templates/edit_form.html
new file mode 100644
--- /dev/null
+++ b/templates/edit_form.html
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>%s</title>
+<style type="text/css">div.indent{margin-left:20px;} div.center{text-align:center;} blockquote{margin-left:20px;background-color:#e0e0e0;} span.underline{text-decoration:underline;}
+</style>
+</head>
+<body>
+<h1>%s</h1>
+<hr/>
+<form method="post"
+ action="%s"
+ enctype="multipart/form-data"
+ accept-charset="UTF-8">
+ <input type=hidden name="savepage" value="%s"/>
+ <textarea wrap="virtual" name="savetext" rows="17" cols="120">%s</textarea>
+ <br/>
+ <input type=submit value="Save"/>
+ <input type=hidden name=formPassword value="%s"/>
+ <input type=reset value="Reset"/>
+ <br/>
+</form>
+<hr/>
+</body>
+</html>
+