site

(djk)
2011-01-30: First pass at configuration UI.

First pass at configuration UI.

diff --git a/build.xml b/build.xml
--- a/build.xml
+++ b/build.xml
@@ -2,7 +2,6 @@
   <property name="src" value="./src" />
   <property name="alien.src" value="./alien/src" />
   <property name="alien.libs" value="./alien/libs" />
-  <property name="plugin.src" value="./plugin/src" />
   <property name="classes" value="./build/classes" />
   <property name="jars" value="./build/jar" />
 
diff --git a/readme.txt b/readme.txt
--- a/readme.txt
+++ b/readme.txt
@@ -1,10 +1,19 @@
-20110126
+------------------------------------------------------------
+This is a my personal work repo. Unless you pull a version
+tagged as a release (none yet), you can assume that code
+may be buggy / broken.
+
+-- djk
+
+------------------------------------------------------------
+
+20110130
 djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 
 WARNING:
 THIS IS RAW ALPHA CODE.
 
-I'm releasing this so that other developers in the Freenet community
+I'm publishing this so that other developers in the Freenet community
 can audit the source code.
 
 DON'T USE IT if violation of your anonymity would put you at risk.
@@ -30,14 +39,28 @@ Edit the script/jfniki.sh to set PRIVATE
 Look at http://127.0.0.1:8083 with your web browser.
 
 BUILD FREENET PLUGIN:
-The plugin code is included in the jfniki.jar but there is no UI for configuration yet,
-so you have to edit the source code and recompile.
-
-Manually edit plugin/src/fniki/plugin/Fniki.java to include your FMS_ID and PRIVATE_FMS_SSK.
-
 ant jar
 load the jar file from ./build/jar/jfniki.jar
 
+Click on the "View Configuration" link and set the "FMS Private SSK" and "FMS ID fields".
+
+
 KNOWN ISSUES:
 o Pages don't auto-refresh. You need to manually reload to see status changes.
   [Freenet ContentFilter is eating meta-refresh???]
+
+
+------------------------------------------------------------
+Dev notes:
+------------------------------------------------------------
+Stopped in the middle of implementing config ui state
+- figure out how to make private ssk wrap
+- implement update msgs (mMsg)
+- implement import / export
+- test in plugin (probably broken at the moment)
+
+------------------------------------------------------------
+
+
+
+
diff --git a/src/fniki/freenet/filter/WikiContentFilter.java b/src/fniki/freenet/filter/WikiContentFilter.java
--- a/src/fniki/freenet/filter/WikiContentFilter.java
+++ b/src/fniki/freenet/filter/WikiContentFilter.java
@@ -76,8 +76,14 @@ class WikiContentFilter implements Conte
      * @throws CommentException If the URI is nvalid or unacceptable in some way.
      */
     public String processURI(String uri, String overrideType, boolean noRelative, boolean inline) throws CommentException {
-        // DCI: understand these parameters!
+        // inline is true for images (which we allow mod URI filtering).
+        // noRelative is true if you must return an absolute URI, which we don't allow.
         System.err.println("processURI(1): " + uri + " : " + overrideType + " : " + noRelative + " : " + inline);
+        if (noRelative) {
+            System.err.println("processURI(1): REJECTED URI because of noRelative.");
+            filterTripped();
+            return null;
+        }
         return processURI(uri, overrideType);
     }
 
diff --git a/src/fniki/freenet/plugin/Fniki.java b/src/fniki/freenet/plugin/Fniki.java
--- a/src/fniki/freenet/plugin/Fniki.java
+++ b/src/fniki/freenet/plugin/Fniki.java
@@ -26,6 +26,7 @@ package fniki.freenet.plugin;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.util.Set;
 
 import freenet.pluginmanager.AccessDeniedPluginHTTPException;
 import freenet.pluginmanager.FredPlugin;
@@ -39,6 +40,7 @@ import freenet.support.api.HTTPRequest;
 
 import fniki.wiki.ArchiveManager;
 import fniki.wiki.Query;
+import fniki.wiki.QueryBase;
 import fniki.wiki.Request;
 import fniki.wiki.WikiApp;
 
@@ -58,23 +60,9 @@ public class Fniki implements FredPlugin
         try {
             ArchiveManager archiveManager = new ArchiveManager();
 
-            // DCI: Parameter handling?
-            archiveManager.setFcpHost("127.0.0.1");
-            archiveManager.setFcpPort(9481);
-
-            archiveManager.setFmsHost("127.0.0.1");
-            archiveManager.setFmsPort(1119);
-
             // YOU MUST SET THESE OR THE PLUGIN WON'T LOAD.
-            archiveManager.setPrivateSSK("FMS_PRIVATE_SSK");
-            archiveManager.setFmsId("FMS_ID");
-
-            archiveManager.setFmsGroup("biss.test000");
-            archiveManager.setBissName("testwiki");
-
-            String fproxyPrefix = "http://127.0.0.1:8888/";
-            boolean enableImages = true;
-
+            // archiveManager.setPrivateSSK("FMS_PRIVATE_SSK");
+            archiveManager.setFmsId("SET_THE_FMS_ID");
             archiveManager.createEmptyArchive();
 
             WikiApp wikiApp = new WikiApp(archiveManager);
@@ -82,8 +70,7 @@ public class Fniki implements FredPlugin
             if (containerPrefix == null) {
                 throw new RuntimeException("Assertion Failure: container_prefix not set!");
             }
-            wikiApp.setFproxyPrefix(fproxyPrefix);
-            wikiApp.setAllowImages(enableImages);
+            wikiApp.setAllowImages(false); // User can enable
 
             // IMPORTANT:
             // HTTP POSTS will be rejected without any useful error message if your form
@@ -107,96 +94,63 @@ public class Fniki implements FredPlugin
 	}
     }
 
-    private static class PluginQuery implements Query {
+    private static class PluginQuery extends QueryBase {
         private final HTTPRequest mParent;
-        private final String mTitle;
-        private final String mAction;
-        private final String mSaveText;
-        private final String mSavePage;
+        private final String mPath;
 
-        PluginQuery(HTTPRequest parent, String path) {
-            mParent = parent;
+        public void readParams() throws IOException {
+            Set<String> allParams = paramsSet();
 
-            String title = path;
-            if (parent.isParameterSet("title")) {
-                title = parent.getParam("title");
+            // Read normal non-multipart params.
+            for (String name : allParams) {
+                if (!mParent.isParameterSet(name)) {
+                    continue;
+                }
+                mParamTable.put(name, mParent.getParam(name));
+                System.err.println("Set Param: " + name + " : " + mParamTable.get(name));
             }
-            mTitle = title;
 
-            // DCI: validate title here
-
-            String action = "view";
-            if (parent.isParameterSet("action")) {
-                action = parent.getParam("action");
-            }
-            mAction = action;
-
-            // Handle multipart form parameters.
-            System.err.println("Dumping list of parts...");
-            String saveText = "";
-            String savePage = "";
+            // Then read multipart params if there are any.
             try {
-                for (String part : parent.getParts()) {
-                    if (part.equals("savetext")) {
-                        // DCI: magic numbers
-                        saveText = new String(parent.getPartAsBytesFailsafe(part, 64 * 1024), "utf-8");
+                for (String part : mParent.getParts()) {
+                    if (!allParams.contains(part)) {
                         continue;
                     }
-                    if (part.equals("savepage")) {
-                        savePage = new String(parent.getPartAsBytesFailsafe(part, 64 * 1024), "utf-8");
-                    }
+
+                    String value = new String(mParent.getPartAsBytesFailsafe(part, 64 * 1024), "utf-8");
+                    mParamTable.put(part, value);
+                    System.err.println("Set multipart Param: " + part + " : " +
+                                       mParamTable.get(part));
                 }
             } catch (UnsupportedEncodingException ue) {
                 // Shouldn't happen.
                 ue.printStackTrace();
             }
-            mSaveText = saveText;
-            mSavePage = savePage;
 
-            parent.freeParts(); // DCI: test!, put in finally?
+            if (!mParamTable.containsKey("action")) {
+                System.err.println("Forced default action to view");
+                mParamTable.put("action", "view");
+            }
 
+            // DCI: title validation?
+            if (!mParamTable.containsKey("title")) {
+                mParamTable.put("title", mPath);
+            }
+            mParent.freeParts(); // DCI: test!, put in finally?
         }
 
-        public boolean containsKey(String paramName) {
-            if (paramName.equals("title") || paramName.equals("action") ||
-                paramName.equals("savetext") || paramName.equals("savepage")) {
-                return true;
-            }
-            return mParent.isParameterSet(paramName);
-        }
-
-        public String get(String paramName) {
-            if (paramName.equals("title")) {
-                return mTitle;
-            }
-            if (paramName.equals("action")) {
-                return mAction;
-            }
-            if (paramName.equals("savetext")) {
-                return mSaveText;
-            }
-            if (paramName.equals("savepage")) {
-                return mSavePage;
-            }
-            if (!containsKey(paramName)) {
-                return null;
-            }
-            return mParent.getParam(paramName);
+        PluginQuery(HTTPRequest parent, String path) throws IOException {
+            super();
+            mParent = parent;
+            mPath = path;
+            readParams();
         }
     }
 
     private static class PluginRequest implements Request {
         private final Query mQuery;
         private final String mPath;
-        PluginRequest(HTTPRequest parent, String containerPrefix) { // DCI throws IOException {
-            for (String key : parent.getParameterNames()) {
-                String value = parent.getParam(key);
-                if (value.length() > 128) {
-                    value = value.substring(0, 128) + "...";
-                }
-                System.err.println(String.format("[%s] => [%s]", key, value));
-            }
-
+        PluginRequest(HTTPRequest parent, String containerPrefix) throws IOException {
             String path = parent.getPath();
             if (!path.startsWith(containerPrefix)) {
                 // This should be impossible because of the way plugin requests are routed.
@@ -235,7 +189,11 @@ public class Fniki implements FredPlugin
         } catch(ChildContainerException serverError) {
             throw new ServerPluginHTTPException(serverError.getMessage(),
                                                 mWikiApp.getString("container_prefix", null));
+        } catch(IOException ioError) {
+            throw new ServerPluginHTTPException(ioError.getMessage(),
+                                                mWikiApp.getString("container_prefix", null));
         }
+
     }
 
     public String handleHTTPGet(HTTPRequest request) throws PluginHTTPException {
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
@@ -26,9 +26,12 @@ package fniki.standalone;
 
 import java.io.IOException;
 import java.io.ByteArrayOutputStream;
+import java.util.Map;
+import java.util.Set;
 import net.freeutils.httpserver.HTTPServer;
 
 import fniki.wiki.Query;
+import fniki.wiki.QueryBase;
 import fniki.wiki.Request;
 import fniki.wiki.WikiApp;
 
@@ -42,10 +45,9 @@ public class FnikiContextHandler impleme
     private final WikiApp mApp;
     private final String mContainerPrefix;
 
-    private static class WikiQuery implements Query {
+    private static class WikiQuery extends QueryBase {
         private final HTTPServer.Request mParent;
-        private final String mSaveText;
-        private final String mSavePage;
+        private final String mPath;
 
         // Hmmmm... can't figure out any other way to know when part is done.
         private final String readAsUtf8(HTTPServer.MultipartIterator.Part part) throws IOException {
@@ -62,52 +64,51 @@ public class FnikiContextHandler impleme
             return new String(baos.toByteArray(), "utf8");
         }
 
+        public void readParams() throws IOException {
+            Set<String> allParams = paramsSet();
 
-        WikiQuery(HTTPServer.Request parent) throws IOException {
-            mParent = parent;
+            // Read normal non-multipart params.
+            Map<String, String> parentParams = mParent.getParams();
+            for (String name : allParams) {
+                if (!parentParams.containsKey(name)) {
+                    continue;
+                }
+                System.err.println("Set Param: " + name + " : " + parentParams.get(name));
+                mParamTable.put(name, parentParams.get(name));
+            }
 
-            String saveText = null;
-            String savePage = null;
-            if (parent.getHeaders().getParams("Content-Type").
+            // Then read multipart params if there are any.
+            if (mParent.getHeaders().getParams("Content-Type").
                 containsKey("multipart/form-data")) {
-                HTTPServer.MultipartIterator iter = new HTTPServer.MultipartIterator(parent);
+                HTTPServer.MultipartIterator iter = new HTTPServer.MultipartIterator(mParent);
                 while (iter.hasNext()) {
                     HTTPServer.MultipartIterator.Part part = iter.next();
-                    if (part.name.equals("savetext")) {
-                        saveText = readAsUtf8(part);
-                    } else if (part.name.equals("savepage")) {
-                        savePage = readAsUtf8(part);
+                    if (!allParams.contains(part.name)) {
+                        continue;
                     }
+                    mParamTable.put(part.name, readAsUtf8(part));
+                    System.err.println("Set multipart Param: " + part.name + " : " +
+                                       mParamTable.get(part.name));
                 }
-                parent.consumeBody();
+                mParent.consumeBody();
             }
-            mSaveText = saveText;
-            mSavePage = savePage;
-        }
-        public boolean containsKey(String paramName) {
-            try {
-                if (paramName.equals("savetext")) {
-                    return mSaveText != null;
-                } else if (paramName.equals("savepage")) {
-                    return mSavePage != null;
-                }
-                return mParent.getParams().containsKey(paramName);
-            } catch (IOException ioe) {
-                return false;
+
+            if (!mParamTable.containsKey("action")) {
+                System.err.println("Forced default action to view");
+                mParamTable.put("action", "view");
+            }
+
+            // DCI: title validation?
+            if (!mParamTable.containsKey("title")) {
+                mParamTable.put("title", mPath);
             }
         }
 
-        public String get(String paramName) {
-            try {
-                if (paramName.equals("savetext")) {
-                    return mSaveText;
-                } else if (paramName.equals("savepage")) {
-                    return mSavePage;
-                }
-                return mParent.getParams().get(paramName);
-            } catch (IOException ioe) {
-                return null;
-            }
+        WikiQuery(HTTPServer.Request parent, String path) throws IOException {
+            super();
+            mParent = parent;
+            mPath = path;
+            readParams();
         }
     }
 
@@ -116,15 +117,6 @@ public class FnikiContextHandler impleme
         private final String mPath;
 
         WikiRequest(HTTPServer.Request parent, String containerPrefix) throws IOException {
-            mQuery = new WikiQuery(parent);
-            for (String key : parent.getParams().keySet()) {
-                String value = parent.getParams().get(key);
-                if (value.length() > 128) {
-                    value = value.substring(0, 128) + "...";
-                }
-                System.err.println(String.format("[%s] => [%s]", key, value));
-            }
-
             String path = parent.getPath();
             if (!path.startsWith(containerPrefix)) {
                 // This should be impossible because of the way HTTPServer routes requests.
@@ -138,26 +130,8 @@ public class FnikiContextHandler impleme
                 path = path.substring(1).trim();
             }
 
-            // DCI: not sure that this stuff belongs here.
-            String title = path;
-            if (mQuery.containsKey("title")) {
-                title = mQuery.get("title");
-            } else {
-                parent.getParams().put("title", title);
-            }
-
-            // DCI: validate title here
-
-            String action = "view";
-            if (mQuery.containsKey("action")) {
-                action = mQuery.get("action");
-            } else {
-                parent.getParams().put("action", action);
-            }
             mPath = path;
-
-
-
+            mQuery = new WikiQuery(parent, path);
         }
 
         public String getPath() { return mPath; }
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
@@ -49,7 +49,7 @@ public class ArchiveManager {
     private final static String FMS_HOST = "127.0.0.1";
     private final static int FMS_PORT = 1119;
     private final static String FMS_GROUP = "biss.test000";
-    private final static String BISS_NAME = "jfniki";
+    private final static String BISS_NAME = "testwiki";
     // Maximum number of versions to read from FMS.
     private final static int MAX_VERSIONS = 50;
 
@@ -94,12 +94,25 @@ public class ArchiveManager {
         mFmsId = value;
     }
 
+    public String getFmsId() { return mFmsId; }
+
     public void setFcpHost(String value) {mFcpHost = value; }
+    public String getFcpHost() { return mFcpHost; }
+
     public void setFcpPort(int value) {mFcpPort = value; }
+    public int getFcpPort() { return mFcpPort; }
+
     public void setFmsHost(String value) { mFmsHost = value; }
+    public String getFmsHost() { return mFmsHost; }
+
     public void setFmsPort(int value) { mFmsPort = value; }
+    public int getFmsPort() { return mFmsPort; }
+
     public void setFmsGroup(String value) { mFmsGroup = value; }
+    public String getFmsGroup() { return mFmsGroup; }
+
     public void setBissName(String value) { mBissName= value; }
+    public String getBissName() { return mBissName; }
 
     // DCI: Fix this to roll back state on exceptions.
     public void load(String uri) throws IOException {
diff --git a/src/fniki/wiki/QueryBase.java b/src/fniki/wiki/QueryBase.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/QueryBase.java
@@ -0,0 +1,66 @@
+/* Base class for writing Query implmentations for a specific HTTP framework.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+
+
+public abstract class QueryBase implements Query {
+    protected Map<String, String> mParamTable = new HashMap<String, String>();
+
+    // MUST contain every parameter used by any ChildContainer.
+    protected final static String PARAMS[] = new String[] {
+        "action",
+        "uri", "goto",
+        "savepage", "savetext",
+        "formPassword",
+        "saved", "discarded", "done",
+        // C&P from SettingConfig.java
+        "fcphost", "fcpport", "fpprefix", "fmshost", "fmsport",
+        "fmsssk", "fmsid", "wikiname", "images",
+    };
+
+    protected static Set<String> paramsSet() {
+        return new HashSet(Arrays.asList(PARAMS));
+    }
+
+    public boolean containsKey(String paramName) {
+        return mParamTable.containsKey(paramName);
+    }
+
+    public String get(String paramName) {
+        return mParamTable.get(paramName);
+    }
+
+    // Subclass should define this and call it once after construction.
+    public abstract void readParams() throws IOException;
+}
diff --git a/src/fniki/wiki/WikiApp.java b/src/fniki/wiki/WikiApp.java
--- a/src/fniki/wiki/WikiApp.java
+++ b/src/fniki/wiki/WikiApp.java
@@ -44,6 +44,7 @@ import fniki.wiki.child.LoadingArchive;
 import fniki.wiki.child.LoadingChangeLog;
 import fniki.wiki.child.LoadingVersionList;
 import fniki.wiki.child.QueryError;
+import fniki.wiki.child.SettingConfig;
 import fniki.wiki.child.Submitting;
 import fniki.wiki.child.WikiContainer;
 
@@ -59,7 +60,8 @@ public class WikiApp implements ChildCon
     private final ChildContainer mQueryError;
     private final ChildContainer mWikiContainer;
 
-    // Containers for asynchronous tasks.
+    // ChildContainers for modal UI states.
+    private final ChildContainer mSettingConfig;
     private final ChildContainer mLoadingVersionList;
     private final ChildContainer mLoadingArchive;
     private final ChildContainer mSubmitting;
@@ -94,6 +96,7 @@ public class WikiApp implements ChildCon
         mQueryError = new QueryError();
         mWikiContainer = new WikiContainer();
 
+        mSettingConfig = new SettingConfig(this, archiveManager);
         mLoadingVersionList = new LoadingVersionList(archiveManager);
         mLoadingArchive = new LoadingArchive(archiveManager);
         mSubmitting = new Submitting(archiveManager);
@@ -195,8 +198,9 @@ public class WikiApp implements ChildCon
         }
 
         System.err.println("WikiApp.routeRequest: " + path);
-        if (path.equals("fniki/submit")) {
-            System.err.println("BC0");
+        if (path.equals("fniki/config")) {
+            return setState(request, mSettingConfig);
+        } else if (path.equals("fniki/submit")) {
             return setState(request, mSubmitting);
         } else if (path.equals("fniki/changelog")) {
             return setState(request, mLoadingChangeLog);
diff --git a/src/fniki/wiki/child/SettingConfig.java b/src/fniki/wiki/child/SettingConfig.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/child/SettingConfig.java
@@ -0,0 +1,205 @@
+/* A UI subcomponent to display and set the configuration.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki.child;
+
+import fniki.wiki.ArchiveManager;
+import fniki.wiki.ChildContainerException;
+import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.ModalContainer;
+import fniki.wiki.Query;
+import fniki.wiki.WikiApp;
+import fniki.wiki.WikiContext;
+
+public class SettingConfig implements ModalContainer {
+    private final WikiApp mWikiApp;
+    private final ArchiveManager mArchiveManager;
+
+    private boolean mFinished = false;
+    private String mMsg = "";
+
+    public SettingConfig(WikiApp wikiApp, ArchiveManager archiveManager) {
+        mWikiApp = wikiApp;
+        mArchiveManager = archiveManager;
+    }
+
+    private void handlePost(WikiContext context) throws ChildContainerException {
+        Query query = context.getQuery();
+        if (query.containsKey("discarded")) {
+            mMsg = "Discarded changes.";
+            return;
+        }
+
+        if (query.containsKey("done")) {
+            // Causes the routing logic in WikiApp.routeRequest to transition
+            // out of this modal ui state.
+            String redirectHref = makeHref(context.makeLink("/fniki/config"),
+                                           "finished", null, null, null);
+            context.raiseRedirect(redirectHref, "Redirecting...");
+        }
+
+        if (!query.containsKey("saved")) {
+            return; // Not sure how this would happen
+        }
+
+        for (String key : FORM_FIELDS) {
+            if (!query.containsKey(key)) {
+                continue;
+            }
+            if (key.equals("fcphost")) { mArchiveManager.setFcpHost(query.get(key)); }
+            if (key.equals("fcpport")) { mArchiveManager.setFcpPort(Integer.parseInt(query.get(key))); }
+
+            if (key.equals("fpprefix")) { mWikiApp.setFproxyPrefix(query.get(key)); }
+
+            if (key.equals("fmshost")) { mArchiveManager.setFmsHost(query.get(key)); }
+            if (key.equals("fmsport")) { mArchiveManager.setFmsPort(Integer.parseInt(query.get(key))); }
+
+            if (key.equals("fmsssk")) { mArchiveManager.setPrivateSSK(query.get(key)); }
+            if (key.equals("fmsid")) { mArchiveManager.setFmsId(query.get(key)); }
+
+            if (key.equals("wikiname")) { mArchiveManager.setBissName(query.get(key)); }
+        }
+
+        // Don't "images" not set in query params when box unchecked.
+        if (query.containsKey("images")) {
+            System.err.println("images: SET");
+            mWikiApp.setAllowImages(true);
+        } else {
+            System.err.println("images: CLEARED");
+            mWikiApp.setAllowImages(false);
+        }
+    }
+
+    private static String noNulls(String text) {
+        if (text == null) {
+            return "";
+        }
+        return text;
+    }
+
+    // DCI: back over this. escape  quotes.
+    public String handle(WikiContext context) throws ChildContainerException {
+        handlePost(context);
+        // DCI: Hmmm... it would be more pedantic to use the context interface for all of these.
+
+        String href = makeHref(context.makeLink("/fniki/config"),
+                               null, null, null, null);
+
+        System.err.println("images allowd: " + context.getInt("allow_images", 0));
+        return String.format(formTemplate(),
+                             href,
+                             noNulls(mArchiveManager.getFcpHost()),
+                             Integer.toString(mArchiveManager.getFcpPort()),
+                             context.getString("fproxy_prefix", "http://127.0.0.1:8888/"),
+                             noNulls(mArchiveManager.getFmsHost()),
+                             Integer.toString(mArchiveManager.getFmsPort()),
+                             noNulls(mArchiveManager.getPrivateSSK()),
+                             noNulls(mArchiveManager.getFmsId()),
+                             noNulls(mArchiveManager.getFmsGroup()),
+                             noNulls(mArchiveManager.getBissName()),
+                             (context.getInt("allow_images", 0) == 1) ? "checked" : "",
+                             // IMPORTANT: Won't work as a plugin without this.
+                             context.getString("form_password", "FORM_PASSWORD_NOT_SET"));
+    }
+
+    public boolean isFinished() {return mFinished; }
+    public void cancel() { mFinished = true; }
+    public void entered(WikiContext context) {
+        mFinished = false;
+        mMsg = "";
+    }
+    public void exited() {}
+
+    //////////////////////////////////////////////////
+    private static final String FORM_FIELDS[] = new String[] {
+        "fcphost", "fcpport", "fpprefix", "fmshost", "fmsport",
+        "fmsssk", "fmsid", "wikiname", "images",
+    };
+
+    private static String formTemplate() {
+        StringBuilder sb = new StringBuilder();
+
+sb.append("<html>\n");
+sb.append("<head>\n");
+sb.append("<title>\n");
+sb.append("  Configuration\n");
+sb.append("</title>\n");
+sb.append("</head>\n");
+sb.append("<body>\n");
+sb.append("<h1>Configuration</h1>\n");
+sb.append("<form method=\"post\" action=\"%s\" enctype=\"multipart/form-data\" accept-charset=\"UTF-8\">\n");
+sb.append("<table>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>Fcp Host</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fcphost\" value=\"%s\" /></td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>Fcp Port</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fcpport\" value=\"%s\" /></td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>Fproxy Prefix</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fpprefix\" size=\"64\" value=\"%s\" /></td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>FMS Host</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fmshost\" value=\"%s\" /></td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>FMS Port</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fmsport\" value=\"%s\" /> </td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>FMS Private SSK</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fmsssk\" size=\"128\" value=\"%s\" /> </td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>FMS ID</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fmsid\" value=\"%s\" /> </td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>FMS Group</td>\n");
+sb.append("    <td><input type=\"text\" name=\"fmsgroup\" value=\"%s\" /> </td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>Wiki Name</td>\n");
+sb.append("    <td><input type=\"text\" name=\"wikiname\" value=\"%s\"/> </td>\n");
+sb.append("  </tr>\n");
+sb.append("  <tr>\n");
+sb.append("    <td>Enable Images: <input type=\"checkbox\" name=\"images\" %s/></td>\n");
+sb.append("  </tr>\n");
+sb.append("  <input type=\"hidden\" name=\"formPassword\" value=\"%s\"/>\n");
+sb.append("</table>\n");
+sb.append("<input name=\"saved\" type=\"submit\" value=\"Save Changes\"/>\n");
+sb.append("<input name=\"discarded\" type=\"submit\" value=\"Discard Changes\"/>\n");
+sb.append("<input name=\"done\" type=\"submit\" value=\"Done\"/>\n");
+sb.append("</form>\n");
+sb.append("\n");
+sb.append("</body>\n");
+sb.append("</html>\n");
+
+        return sb.toString();
+    }
+}
+
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
@@ -198,6 +198,9 @@ public class WikiContainer implements Ch
         buffer.append(makeLocalLink(context, "fniki/getversions", "confirm", "Discover"));
         buffer.append(" other recent version.<br>");
 
+        buffer.append(makeLocalLink(context, "fniki/config", "view", "View"));
+        buffer.append(" configuration.<br>");
+
         buffer.append("</body></html>");
     }
 
@@ -218,10 +221,6 @@ public class WikiContainer implements Ch
         //            the Freenet ContentFilter rewrites the encoding in all forms
         //            to this value.
         buffer.append("multipart/form-data");
-
-        System.err.println("Sending form encoding: " + context.getString("form_encoding", "application/x-www-form-urlencoded"));
-
-
         buffer.append("\" accept-charset=\"UTF-8\">\n");
 
         buffer.append("<input type=hidden name=\"savepage\" value=\"");