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> +