site

(djk)
2011-02-05: Importing and exporting config. Bugfixes.

Importing and exporting config. Bugfixes.

diff --git a/quickstart.txt b/quickstart.txt
new file mode 100644
--- /dev/null
+++ b/quickstart.txt
@@ -0,0 +1,41 @@
+//You are seeing this Quick Start because the wiki has no Front_Page. It will disappear as soon as you edit and save the page. //
+----
+
+== Quick Start ==
+
+===Configuration===
+# Click on the "View" link below to view (and edit) the configuration.
+# Set the "FMS ID" to the human readable part of your FMS ID (everything before the '@').
+# Set the FMS Private SSK to your private FMS SSK (see below if you don't know how to find this).
+# 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===
+Click the "Discover" link below to search for other versions of the wiki.
+
+=== Submitting ===
+Use the "Submit" link below to submit your changes.  It may take a long time for other people to see them.
+
+=== Finding Your Private SSK ===
+# Go to http://127.0.0.1:18080/localidentities.htm in the FMS web interface and click the "Export Identities" button
+to save your FMS indentities to a file.
+
+# In the text editor of your choice, open the file you saved above and look for the Name and PrivateKey values for the identity you want to use.
+
+In the example identity snippet below, the FMS ID value would be:\\
+SomeUser
+
+and the FMS Private Key would be: \\
+SSK@YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,AQECAAE/
+
+----
+{{{
+<Identity>
+   <Name><![CDATA[SomeUser]]></Name>
+   <PublicKey>SSK@XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,AQACAAE/</PublicKey>
+   <PrivateKey>SSK@YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,AQECAAE/</PrivateKey>
+   <SingleUse>false</SingleUse>
+   <PublishTrustList>false</PublishTrustList>
+   <PublishBoardList>false</PublishBoardList>
+   <PublishFreesite>false</PublishFreesite>
+</Identity>
+}}}
diff --git a/readme.txt b/readme.txt
--- a/readme.txt
+++ b/readme.txt
@@ -1,27 +1,19 @@
-------------------------------------------------------------
-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
+20110205
 djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 
 WARNING:
-THIS IS RAW ALPHA CODE.
-
-I'm publishing this so that other developers in the Freenet community
-can audit the source code.
+This is beta code.  I've taken reasonable precautions to make sure it is
+safe, including running the Freenet content filter over the final output
+to trap dangerous HTML. However, this is new code that was written fast and
+hasn't been audited by *anyone*.
 
 DON'T USE IT if violation of your anonymity would put you at risk.
 
 ABOUT:
 * jfniki is an experimental serverless wiki implementation which runs over Freenet / FMS.
-* It is written in Java and has no external build dependencies.
+* It is written in Java and has no external build dependencies except for freenet.jar.
 * jfniki is INCOMPATIBLE with the existing server based python fniki implementation.
+* It can run either as a standalone web app or as a Freenet Plugin.
 
 REQUIREMENTS:
 ant
@@ -31,36 +23,32 @@ Access to a running Freenet Node and FMS
 BUILD:
 ant jar
 
-RUN:
-Edit the script/jfniki.sh to set PRIVATE_FMS_SSK and FMS_ID correctly and comment out the warning lines.
-
-./script/jfniki.sh
+RUN STAND ALONE:
+./script/jfniki.sh 8083
 
 Look at http://127.0.0.1:8083 with your web browser.
 
-BUILD FREENET PLUGIN:
-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.
+If you want you can use the "Export" button on the Configuration page to export your
+configuration to a file.
 
-Click on the "View Configuration" link and set the "FMS Private SSK" and "FMS ID fields".
+Once you've done that you can start the script with the saved configuration. i.e.:
 
+./script/jfniki.sh path_to_your_saved_config
+
+will start the stand alone app on the same port you used before.
+
+RUN AS A FREENET PLUGIN:
+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.
+
+OTHER DOC:
+See quickstart.txt in this directory (The default page when an empty wiki is displayed).
 
 KNOWN ISSUES:
 o Pages don't auto-refresh. You need to manually reload to see status changes.
-  [Freenet ContentFilter is eating meta-refresh???]
+  [Freenet ContentFilter is eating meta-refresh??? Fixed now with WikiContentFilter.EXCEPTIONS hack]
+o "Cancel" sometimes fails. [WORKAROUND: load and unload the plugin / kill restart the stand alone app.]
 
 
-------------------------------------------------------------
-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/script/jfniki.sh b/script/jfniki.sh
--- a/script/jfniki.sh
+++ b/script/jfniki.sh
@@ -4,30 +4,8 @@
 # You should be able to copy this script anywhere you want. Just make symlinks
 # to the jfniki.jar and freenet.jar files in the same directory.
 
-# TIP:
-# Look in the XML file generate by FMS when you export and identity on
-# the "Local Identities"  page to find these values.
+export set JAR_NAME="jfniki.jar"
 
-# MUST set this to post. i.e. you can run read only without it if you want.
-# The <PrivateKey> value for the FMS identity you want post wiki submissions with.
-export set PRIVATE_FMS_SSK="SSK@XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,AQECAAE/"
-
-# MUST set this to read new version from FMS. i.e. Don't try to run with setting it!
-# The correponding <name> value for that private key.
-export set FMS_ID="YOUR_FMS_HERE"
-
-# FAIL in an obvious way until properly configured.
-echo "MANUAL CONFIGURATION REQUIRED!"
-echo "Edit PRIVATE_FMS_SSK and FMS_ID in this script, then comment out these 3 lines."
-exit -1
-
-export set ENABLE_IMAGES=1
-export set LISTEN_PORT=8083
-
-export set JAR_NAME="jfniki.jar"
-# Look for the jfniki.jar file in the build dir.
-# If you want to move it to somewhere else, modify the line below.
-#export set JAR_PATH="${0%%/*}/../build/jar"
 export set SCRIPT_DIR=`dirname $0`
 
 export set JAR_PATH="${SCRIPT_DIR}/../build/jar"
@@ -74,33 +52,6 @@ fi
 
 echo "Using freenet.jar: ${FN_JAR_FILE}"
 echo
-
-# FCP configuration
-export set FCP_HOST="127.0.0.1"
-export set FCP_PORT=9481
-
-# FMS configuration
-export set FMS_HOST="127.0.0.1"
-export set FMS_PORT=1119
-
-export set FMS_GROUP="biss.test000"
-export set WIKI_NAME="testwiki"
-
-# fproxy configuration.
-export set FPROXY_PREFIX="http://127.0.0.1:8888/"
-
 export set JAVA_CMD="java"
 
-${JAVA_CMD} -classpath ${JAR_FILE}:${FN_JAR_FILE} fniki.standalone.ServeHttp \
-    ${LISTEN_PORT} \
-    ${FCP_HOST} \
-    ${FCP_PORT} \
-    ${FMS_HOST} \
-    ${FMS_PORT} \
-    ${PRIVATE_FMS_SSK} \
-    "${FMS_ID}" \
-    ${FMS_GROUP} \
-    ${WIKI_NAME} \
-    ${FPROXY_PREFIX} \
-    ${ENABLE_IMAGES} \
-    $1
+${JAVA_CMD} -classpath ${JAR_FILE}:${FN_JAR_FILE} fniki.standalone.ServeHttp $1 $2
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
@@ -131,6 +131,34 @@ class WikiContentFilter implements Conte
     public String processTag(ParsedTag pt) { return null; }
     ////////////////////////////////////////////////////////////
 
+    // One off hacks to allow specific cases mangled by the filter.
+    private final static String EXCEPTIONS[] = new String[] {
+        // Mangled form
+        "<input name=\"import\" type=\"submit\" value=\"Import Configuration\" />\n\n<hr>\n",
+        // Allowed
+        "<input name=\"import\" type=\"submit\" value=\"Import Configuration\"/>\n" +
+        "<input type=\"file\" name=\"upload\" size=\"64\">\n<hr>\n",
+        // Removed meta refresh
+        "html><head>\n\n<title>",
+        // Allowed.
+        "html><head><meta http-equiv=\"refresh\" content=\"15\" /><title>\n"
+    };
+
+    // Allow a few safe, specific exceptions through the content filter.
+    public String postProcess(String filtered, String unfiltered) {
+        // System.err.println("--- unfiltered ---");
+        // System.err.println(unfiltered);
+        // System.err.println("--- filtered ---");
+        // System.err.println(filtered);
+        // System.err.println("---");
+        int index = 0;
+        while (index < EXCEPTIONS.length) {
+            filtered = filtered.replace(EXCEPTIONS[index], EXCEPTIONS[index + 1]);
+            index += 2;
+        }
+        return filtered;
+    }
+
     public String filter(String html) throws ServerErrorException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 
@@ -140,7 +168,7 @@ class WikiContentFilter implements Conte
                                                            baos, "text/html",
                                                            UTF8,
                                                            this);
-            return new String(baos.toByteArray(), UTF8);
+            return postProcess(new String(baos.toByteArray(), UTF8), html);
         } catch (UnsafeContentTypeException ucte) {
             ucte.printStackTrace();
             throw new ServerErrorException("BUG: Generated dangerous html(0)??? But we caught it :-)");
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
@@ -29,6 +29,7 @@ import java.io.UnsupportedEncodingExcept
 import java.util.Set;
 
 import freenet.pluginmanager.AccessDeniedPluginHTTPException;
+import freenet.pluginmanager.DownloadPluginHTTPException;
 import freenet.pluginmanager.FredPlugin;
 import freenet.pluginmanager.FredPluginHTTP;
 import freenet.pluginmanager.FredPluginThreadless;
@@ -45,13 +46,14 @@ import fniki.wiki.Request;
 import fniki.wiki.WikiApp;
 
 import fniki.wiki.AccessDeniedException;
+import fniki.wiki.DownloadException;
 import fniki.wiki.NotFoundException;
 import fniki.wiki.RedirectException;
 import fniki.wiki.ChildContainerException;
 
 public class Fniki implements FredPlugin, FredPluginHTTP, FredPluginThreadless {
     private WikiApp mWikiApp;
-
+    private String mContainerPrefix;
     public void terminate() {
         System.err.println("terminating...");
     }
@@ -59,18 +61,13 @@ public class Fniki implements FredPlugin
     public void runPlugin(PluginRespirator pr) {
         try {
             ArchiveManager archiveManager = new ArchiveManager();
-
-            // YOU MUST SET THESE OR THE PLUGIN WON'T LOAD.
-            // archiveManager.setPrivateSSK("FMS_PRIVATE_SSK");
-            archiveManager.setFmsId("SET_THE_FMS_ID");
             archiveManager.createEmptyArchive();
 
             WikiApp wikiApp = new WikiApp(archiveManager);
-            final String containerPrefix = wikiApp.getString("container_prefix", null);
-            if (containerPrefix == null) {
+            if (wikiApp.getString("container_prefix", null) == null) {
                 throw new RuntimeException("Assertion Failure: container_prefix not set!");
             }
-            wikiApp.setAllowImages(false); // User can enable
+            mContainerPrefix = wikiApp.getString("container_prefix", null);
 
             // IMPORTANT:
             // HTTP POSTS will be rejected without any useful error message if your form
@@ -172,28 +169,27 @@ public class Fniki implements FredPlugin
     }
 
     public String handle(HTTPRequest request) throws PluginHTTPException {
-        // DCI: cleanup container_prefix usage
+        try {
+            mWikiApp.setRequest(new PluginRequest(request, mContainerPrefix));
+            return mWikiApp.handle(mWikiApp);
 
-        try {
-            mWikiApp.setRequest(new PluginRequest(request, mWikiApp.getString("container_prefix", null)));
-            return mWikiApp.handle(mWikiApp);
+            // IMPORTANT: Look at these catch blocks carefully. They bypass the freenet ContentFilter.
         } catch(AccessDeniedException accessDenied) {
-            throw new AccessDeniedPluginHTTPException(accessDenied.getMessage(),
-                                                      mWikiApp.getString("container_prefix", null));
+            throw new AccessDeniedPluginHTTPException(accessDenied.getMessage(), mContainerPrefix);
         } catch(NotFoundException notFound) {
-            throw new NotFoundPluginHTTPException(notFound.getMessage(),
-                                                  mWikiApp.getString("container_prefix", null));
+            throw new NotFoundPluginHTTPException(notFound.getMessage(), mContainerPrefix);
         } catch(RedirectException redirected) {
-            throw new RedirectPluginHTTPException(redirected.getMessage(),
-                                              redirected.getLocation());
+            throw new RedirectPluginHTTPException(redirected.getMessage(), redirected.getLocation());
+        } catch(DownloadException forceDownload) {
+            // This is to allow exporting the configuration.
+            throw new DownloadPluginHTTPException(forceDownload.mData,
+                                                  forceDownload.mFilename,
+                                                  forceDownload.mMimeType);
         } catch(ChildContainerException serverError) {
-            throw new ServerPluginHTTPException(serverError.getMessage(),
-                                                mWikiApp.getString("container_prefix", null));
+            throw new ServerPluginHTTPException(serverError.getMessage(), mContainerPrefix);
         } catch(IOException ioError) {
-            throw new ServerPluginHTTPException(ioError.getMessage(),
-                                                mWikiApp.getString("container_prefix", null));
+            throw new ServerPluginHTTPException(ioError.getMessage(), mContainerPrefix);
         }
-
     }
 
     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,6 +26,8 @@ package fniki.standalone;
 
 import java.io.IOException;
 import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+
 import java.util.Map;
 import java.util.Set;
 import net.freeutils.httpserver.HTTPServer;
@@ -36,6 +38,7 @@ import fniki.wiki.Request;
 import fniki.wiki.WikiApp;
 
 import fniki.wiki.AccessDeniedException;
+import fniki.wiki.DownloadException;
 import fniki.wiki.NotFoundException;
 import fniki.wiki.RedirectException;
 import fniki.wiki.ChildContainerException;
@@ -174,6 +177,27 @@ public class FnikiContextHandler impleme
             } catch(RedirectException redirected) {
                 resp.redirect(redirected.getLocation(), false);
                 return 0;
+            } catch(DownloadException forceDownload) {
+                try {
+                    resp.getHeaders().add("Content-disposition",
+                                          String.format("attachment; filename=%s", forceDownload.mFilename));
+                    resp.sendHeaders(200, forceDownload.mData.length, -1,
+                                     null, forceDownload.mMimeType, null);
+                    OutputStream body = resp.getBody();
+                    if (body == null) {
+                        return 0; // hmmm... getBody() can return null.
+                    }
+                    try {
+                        body.write(forceDownload.mData);
+                    } finally {
+                        body.close();
+                    }
+                    return 0;
+                } catch (IOException ioe) {
+                    // Totally hosed. We already sent the headers so we can't send a response.
+                    ioe.printStackTrace();
+                    return 0;
+                }
             } catch(ChildContainerException serverError) {
                 // This also handles ServerErrorException.
                 resp.sendError(500, serverError.getMessage());
diff --git a/src/fniki/standalone/ServeHttp.java b/src/fniki/standalone/ServeHttp.java
--- a/src/fniki/standalone/ServeHttp.java
+++ b/src/fniki/standalone/ServeHttp.java
@@ -24,9 +24,12 @@
 package fniki.standalone;
 
 import java.io.IOException;
+import java.io.FileInputStream;
 
 import net.freeutils.httpserver.HTTPServer;
 
+import wormarc.IOUtil;
+import fniki.wiki.Configuration;
 import fniki.wiki.WikiApp;
 import fniki.wiki.ArchiveManager;
 
@@ -41,64 +44,43 @@ public class ServeHttp {
         "Launch a wiki viewer / editor on localhost.\n" +
         "This is experimental code. Use it at your own peril.\n\n" +
         "USAGE:\n" +
-        "java -jar jfniki.jar <listen_port> <fcp_host> <fcp_port> <fms_host> <fms_port> " +
-        "<private_fms_ssk> <fms_id> <fms_group> <wiki_name> <fproxy_prefix> <enable_images> [uri]\n\n";
+        "java -jar jfniki.jar <listen_port>\n" +
+        "or\n" +
+        "java -jar jfniki.jar <config_file>\n\n" +
+        "NOTE:\nfreenet.jar MUST be in your classpath.\n\n" +
+        "EXAMPLES:\n" +
+        "java -jar jfniki.jar ~/saved_jfniki.cfg\n\n" +
+        "java -jar jfniki.jar 8099\n\n";
 
 
-    private final static String ARG_NAMES[] = new String[] {
-        "<listen_port>", "<fcp_host>", "<fcp_port>", "<fms_host>","<fms_port>",
-        "<private_fms_ssk>", "<fms_id>", "<fms_group>", "<wiki_name>",
-        "<fproxy_prefix>", "<enable_images>", "[uri]", };
-
-    public static void debugDumpArgs(String[] args) {
-        for (int index = 0; index < args.length; index++) {
-            String name = "???";
-            if (index < ARG_NAMES.length) {
-                name = ARG_NAMES[index];
-            }
-            System.out.println(String.format("[%d]:{%s}[%s]", index, name, args[index]));
-        }
-    }
-    public static int asInt(String value) { return Integer.parseInt(value); }
     public static void main(String[] args) throws Exception {
-        if (args.length < 11) {
+        if (args.length != 1) {
             System.err.println(HELP_TEXT);
             System.exit(-1);
         }
-        debugDumpArgs(args);
-        int listenPort = Integer.parseInt(args[0]);
 
         ArchiveManager archiveManager = new ArchiveManager();
+        archiveManager.createEmptyArchive();
 
-        archiveManager.setFcpHost(args[1]);
-        archiveManager.setFcpPort(asInt(args[2]));
+        WikiApp wikiApp = new WikiApp(archiveManager);
 
-        archiveManager.setFmsHost(args[3]);
-        archiveManager.setFmsPort(asInt(args[4]));
-
-        archiveManager.setPrivateSSK(args[5]);
-        archiveManager.setFmsId(args[6]);
-
-        archiveManager.setFmsGroup(args[7]);
-        archiveManager.setBissName(args[8]);
-
-        String fproxyPrefix = args[9];
-        boolean enableImages = (args[10].equals("1") || args[10].toLowerCase().equals("true")) ? true : false;
-
-        if (args.length > 11) {
-            archiveManager.load(args[11]);
-        } else {
-            archiveManager.createEmptyArchive();
+        if(wikiApp.getString("container_prefix", null) == null) {
+            throw new RuntimeException("Assertion Failure: container_prefix not set!");
         }
 
-        WikiApp wikiApp = new WikiApp(archiveManager);
+        try {
+            // Try to parse the argument as an integer listen port.
+            wikiApp.setListenPort(Integer.parseInt(args[0]));
+
+        } catch (NumberFormatException nfe) {
+            System.out.println("Reading configuration from: " + args[0]);
+            Configuration config =
+                Configuration.fromStringRep(IOUtil.readUtf8StringAndClose(new FileInputStream(args[0])));
+            wikiApp.setConfiguration(config);
+        }
+
+        int listenPort = wikiApp.getInt("listen_port", WikiApp.LISTEN_PORT);
         final String containerPrefix = wikiApp.getString("container_prefix", null);
-        if (containerPrefix == null) {
-            throw new RuntimeException("Assertion Failure: container_prefix not set!");
-        }
-        wikiApp.setFproxyPrefix(fproxyPrefix);
-        wikiApp.setAllowImages(enableImages);
-
         // Redirect non-routed requests to the wiki app.
         HTTPServer.ContextHandler defaultRedirect = new HTTPServer.ContextHandler() {
                 public int serve(HTTPServer.Request req, HTTPServer.Response resp) throws IOException {
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
@@ -43,13 +43,13 @@ import wormarc.RootObjectKind;
 import wormarc.io.FreenetIO;
 
 public class ArchiveManager {
-    private final static String FCP_HOST = "127.0.0.1";
-    private final static int FCP_PORT = 9481;
+    public final static String FCP_HOST = "127.0.0.1";
+    public final static int FCP_PORT = 9481;
 
-    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 = "testwiki";
+    public final static String FMS_HOST = "127.0.0.1";
+    public final static int FMS_PORT = 1119;
+    public final static String FMS_GROUP = "biss.test000";
+    public final static String BISS_NAME = "testwiki";
     // Maximum number of versions to read from FMS.
     private final static int MAX_VERSIONS = 50;
 
diff --git a/src/fniki/wiki/Configuration.java b/src/fniki/wiki/Configuration.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/Configuration.java
@@ -0,0 +1,170 @@
+/* Configuration settings for the WikiApp
+ *
+ * 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;
+
+public final class Configuration {
+    public final static class ConfigurationException extends IllegalArgumentException {
+        protected ConfigurationException(String msg) {
+            super(msg);
+        }
+    }
+
+    public final int mListenPort;
+    public final String mFcpHost;
+    public final int mFcpPort;
+    public final String mFproxyPrefix;
+    public final boolean mAllowImages;
+
+    public final String mFmsHost;
+    public final int mFmsPort;
+    public final String mFmsId;
+    public final String mFmsSsk;
+    public final String mFmsGroup;
+
+    public final String mWikiName;
+
+    public Configuration(int listenPort,
+                         String fcpHost,
+                         int fcpPort,
+                         String fproxyPrefix,
+                         boolean allowImages,
+                         String fmsHost,
+                         int fmsPort,
+                         String fmsId,
+                         String fmsSsk,
+                         String fmsGroup,
+                         String wikiName) {
+        mListenPort = listenPort;
+        mFcpHost = noNulls(fcpHost);
+        mFcpPort = fcpPort;
+        mFproxyPrefix = noNulls(fproxyPrefix);
+        mAllowImages = allowImages;
+        mFmsHost = noNulls(fmsHost);
+        mFmsPort = fmsPort;
+        mFmsId = noNulls(fmsId);
+        mFmsSsk = noNulls(fmsSsk);
+        mFmsGroup = noNulls(fmsGroup);
+        mWikiName = noNulls(wikiName);
+    }
+
+    private static String noNulls(String text) {
+        if (text == null) {
+            return "";
+        }
+        return text;
+    }
+
+    private static void checkSet(String value, String field) throws ConfigurationException {
+        if (value == null || value.trim().length() == 0) {
+            throw new ConfigurationException(String.format("%s is not set.", field));
+        }
+    }
+
+    private static void checkSet(int value, String field) throws ConfigurationException {
+        if (value <= 0) {
+            throw new ConfigurationException(String.format("%s is not set.", field));
+        }
+    }
+
+    public void validate() throws ConfigurationException {
+        // LATER: Do better. Unlocalized strings which show up in the UI.
+        checkSet(mListenPort, "Listen Port");
+        checkSet(mFcpHost, "FCP Host");
+        checkSet(mFcpPort, "FCP Port");
+        checkSet(mFproxyPrefix, "Fproxy Prefix");
+        //mAllowImages,
+        checkSet(mFmsHost, "FMS Host");
+        checkSet(mFmsPort, "FMS Port");
+        checkSet(mFmsId, "FMS Id");
+        checkSet(mFmsSsk, "FMS Private SSK");
+        checkSet(mFmsGroup, "FMS Group");
+        checkSet(mWikiName, "Wiki Name");
+
+        if (!mFmsSsk.startsWith("SSK@") || !mFmsSsk.endsWith(",AQECAAE/")) {
+            throw new ConfigurationException("The private SSK value must start with 'SSK@' " +
+                                             "and end with ',AQECAAE/'.");
+        }
+        if (mFmsId.indexOf("@") != -1) {
+            throw new ConfigurationException("FMS Id Should only include the part before the '@'.");
+        }
+        if (!mFproxyPrefix.startsWith("http") || !mFproxyPrefix.endsWith("/")) {
+            throw new ConfigurationException("The fproxy prefix must start with 'http' and end with '/'.");
+        }
+
+        fromStringRep(toStringRep()); // traps '\n's in values.
+    }
+
+    public String toStringRep() throws ConfigurationException {
+        return String.format("%d\n%s\n%d\n%s\n%d\n%s\n%d\n%s\n%s\n%s\n%s\n",
+                             mListenPort,
+                             mFcpHost,
+                             mFcpPort,
+                             mFproxyPrefix,
+                             mAllowImages ? 1 : 0,
+                             mFmsHost,
+                             mFmsPort,
+                             mFmsId,
+                             mFmsSsk,
+                             mFmsGroup,
+                             mWikiName);
+    }
+
+    // Doesn't validate.
+    public static Configuration fromStringRep(String text) {
+        String[] fields = text.split("\n");
+        if (fields.length != 11) {
+            throw new ConfigurationException("Couldn't parse configuration.");
+        }
+        int listenPort = -1;
+        try { listenPort = Integer.parseInt(fields[0]); } catch (NumberFormatException nfe) { /*NOP*/ }
+        String fcpHost = fields[1];
+        int fcpPort = -1;
+        try { fcpPort = Integer.parseInt(fields[2]); } catch (NumberFormatException nfe) { /*NOP*/ }
+        String fproxyPrefix = fields[3];
+        boolean allowImages = fields[4].equals("1");
+        String fmsHost = fields[5];
+        int fmsPort = -1;
+        try { fmsPort = Integer.parseInt(fields[6]); } catch (NumberFormatException nfe) { /*NOP*/ }
+        String fmsId = fields[7];
+        String fmsSsk = fields[8];
+        String fmsGroup = fields[9];
+        String wikiName = fields[10];
+
+        Configuration config = new Configuration(listenPort,
+                                                 fcpHost,
+                                                 fcpPort,
+                                                 fproxyPrefix,
+                                                 allowImages,
+                                                 fmsHost,
+                                                 fmsPort,
+                                                 fmsId,
+                                                 fmsSsk,
+                                                 fmsGroup,
+                                                 wikiName);
+        return config;
+    }
+
+    public String toString() { return toStringRep().replace("\n", "|"); }
+}
diff --git a/src/fniki/wiki/DownloadException.java b/src/fniki/wiki/DownloadException.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/DownloadException.java
@@ -0,0 +1,40 @@
+/* Exception raised for HTTP 302 redirects.
+ *
+ * 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;
+
+public class DownloadException extends ChildContainerException {
+    public final String mFilename;
+    public final String mMimeType;
+    public final byte[] mData;
+
+    public DownloadException(byte[] data, String filename, String mimeType) {
+        super("Download of: " + filename);
+        mData = data;
+        mFilename = filename;
+        mMimeType = mimeType;
+    }
+}
+
diff --git a/src/fniki/wiki/QueryBase.java b/src/fniki/wiki/QueryBase.java
--- a/src/fniki/wiki/QueryBase.java
+++ b/src/fniki/wiki/QueryBase.java
@@ -32,8 +32,6 @@ 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>();
 
@@ -43,10 +41,10 @@ public abstract class QueryBase implemen
         "uri", "goto",
         "savepage", "savetext",
         "formPassword",
-        "saved", "discarded", "done",
-        // C&P from SettingConfig.java
+        // Configuration stuff.
+        "defaults", "done", "import", "export", "upload",
         "fcphost", "fcpport", "fpprefix", "fmshost", "fmsport",
-        "fmsssk", "fmsid", "wikiname", "images",
+        "fmsssk", "fmsid", "fmsgroup", "wikiname", "images",
     };
 
     protected static Set<String> paramsSet() {
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
@@ -52,6 +52,22 @@ import fniki.freenet.filter.ContentFilte
 
 // Aggregates a bunch of other ChildContainers and runs UI state machine.
 public class WikiApp implements ChildContainer, WikiContext {
+    public final static int LISTEN_PORT = 8083;
+    private final static String FPROXY_PREFIX = "http://127.0.0.1:8888/";
+    private final static boolean ALLOW_IMAGES = false;
+    private final static Configuration DEFAULT_CONFIG =
+        new Configuration(LISTEN_PORT,
+                          ArchiveManager.FCP_HOST,
+                          ArchiveManager.FCP_PORT,
+                          FPROXY_PREFIX,
+                          ALLOW_IMAGES,
+                          ArchiveManager.FMS_HOST,
+                          ArchiveManager.FMS_PORT,
+                          "",
+                          "",
+                          ArchiveManager.FMS_GROUP,
+                          ArchiveManager.BISS_NAME);
+
     // Delegate to implement link, image and macro handling in wikitext.
     private final FreenetWikiTextParser.ParserDelegate mParserDelegate;
 
@@ -79,9 +95,11 @@ public class WikiApp implements ChildCon
     // over all output before serving it.
     private ContentFilter mFilter;
 
-    private String mFproxyPrefix = "http://127.0.0.1:8888/";
-    private boolean mAllowImages = true;
+    private String mFproxyPrefix = FPROXY_PREFIX;
+    private boolean mAllowImages = ALLOW_IMAGES;
     private String mFormPassword;
+    private int mListenPort = LISTEN_PORT;
+
 
     // final because it is called from the ctor.
     private final void resetContentFilter() {
@@ -96,7 +114,7 @@ public class WikiApp implements ChildCon
         mQueryError = new QueryError();
         mWikiContainer = new WikiContainer();
 
-        mSettingConfig = new SettingConfig(this, archiveManager);
+        mSettingConfig = new SettingConfig();
         mLoadingVersionList = new LoadingVersionList(archiveManager);
         mLoadingArchive = new LoadingArchive(archiveManager);
         mSubmitting = new Submitting(archiveManager);
@@ -124,6 +142,11 @@ public class WikiApp implements ChildCon
         mFormPassword = value;
     }
 
+    // Doesn't change port, just sets value returned by getInt("listen_port", -1)
+    public void setListenPort(int value) {
+        mListenPort = value;
+    }
+
     private ChildContainer setState(WikiContext context, ChildContainer container) {
         if (mState == container) {
             return mState;
@@ -197,6 +220,7 @@ public class WikiApp implements ChildCon
             }
         }
 
+        // DCI: Fix. Use a hashmap of paths -> instances for static paths
         System.err.println("WikiApp.routeRequest: " + path);
         if (path.equals("fniki/config")) {
             return setState(request, mSettingConfig);
@@ -357,6 +381,8 @@ public class WikiApp implements ChildCon
             return containerPrefix();
         } else if (keyName.equals("form_password") && mFormPassword != null) {
             return mFormPassword;
+        } else if (keyName.equals("default_wikitext")) {
+            return getDefaultWikiText();
         }
 
         return defaultValue;
@@ -366,9 +392,48 @@ public class WikiApp implements ChildCon
         if (keyName.equals("allow_images")) {
             return mAllowImages ? 1 : 0;
         }
+        if (keyName.equals("listen_port")) {
+            return mListenPort;
+        }
+
         return defaultValue;
     }
 
+    // Can return an invalid configuration. e.g. if fms id and private ssk are not set.
+    public Configuration getConfiguration() {
+        // Converts null values to ""
+        return new Configuration(getInt("listen_port", LISTEN_PORT), //DCI: clean up magic numbers
+                                 mArchiveManager.getFcpHost(),
+                                 mArchiveManager.getFcpPort(),
+                                 getString("fproxy_prefix", FPROXY_PREFIX),
+                                 mAllowImages,
+                                 mArchiveManager.getFmsHost(),
+                                 mArchiveManager.getFmsPort(),
+                                 mArchiveManager.getFmsId(),
+                                 mArchiveManager.getPrivateSSK(),
+                                 mArchiveManager.getFmsGroup(),
+                                 mArchiveManager.getBissName());
+    }
+
+    public Configuration getDefaultConfiguration() { return DEFAULT_CONFIG; }
+
+    // For setting data from forms and restoring saved settings.
+    // throws unchecked Configuration.ConfigurationException
+    public void setConfiguration(Configuration config) {
+        config.validate();
+        setListenPort(config.mListenPort);
+        mArchiveManager.setFcpHost(config.mFcpHost);
+        mArchiveManager.setFcpPort(config.mFcpPort);
+        setFproxyPrefix(config.mFproxyPrefix);
+        setAllowImages(config.mAllowImages);
+        mArchiveManager.setFmsHost(config.mFmsHost);
+        mArchiveManager.setFmsPort(config.mFmsPort);
+        mArchiveManager.setFmsId(config.mFmsId);
+        mArchiveManager.setPrivateSSK(config.mFmsSsk);
+        mArchiveManager.setFmsGroup(config.mFmsGroup);
+        mArchiveManager.setBissName(config.mWikiName);
+    }
+
     // DCI: Think this through.
     public String makeLink(String containerRelativePath) {
         // Hacks to find bugs
@@ -402,6 +467,10 @@ public class WikiApp implements ChildCon
         throw new ServerErrorException(msg);
     }
 
+    public void raiseDownload(byte[] data, String filename, String mimeType) throws DownloadException {
+        throw new DownloadException(data, filename, mimeType);
+    }
+
     public void logError(String msg, Throwable t) {
         if (msg == null) {
             msg = "null";
@@ -427,4 +496,52 @@ public class WikiApp implements ChildCon
         }
         mRequest = request;
     }
+
+    private static String getDefaultWikiText() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("//You are seeing this Quick Start because the wiki has no Front_Page. It will disappear as soon as you edit and save the page. //\n");
+        sb.append("----\n");
+        sb.append("\n");
+        sb.append("== Quick Start ==\n");
+        sb.append("\n");
+        sb.append("===Configuration===\n");
+        sb.append("# Click on the \"View\" link below to view (and edit) the configuration.\n");
+        sb.append("# Set the \"FMS ID\" to the human readable part of your FMS ID (everything before the '@').\n");
+        sb.append("# Set the FMS Private SSK to your private FMS SSK (see below if you don't know how to find this).\n");
+        sb.append("# 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.\n");
+        sb.append("# Click the \"Done\" button to save the configuration changes.\n");
+        sb.append("\n");
+        sb.append("=== Finding Other Versions===\n");
+        sb.append("Click the \"Discover\" link below to search for other versions of the wiki.\n");
+        sb.append("\n");
+        sb.append("=== Submitting ===\n");
+        sb.append("Use the \"Submit\" link below to submit your changes.  It may take a long time for other people to see them.\n");
+        sb.append("\n");
+        sb.append("=== Finding Your Private SSK ===\n");
+        sb.append("# Go to http://127.0.0.1:18080/localidentities.htm in the FMS web interface and click the \"Export Identities\" button\n");
+        sb.append("to save your FMS indentities to a file.\n");
+        sb.append("\n");
+        sb.append("# In the text editor of your choice, open the file you saved above and look for the Name and PrivateKey values for the identity you want to use.\n");
+        sb.append("\n");
+        sb.append("In the example identity snippet below, the FMS ID value would be:\\\\ \n");
+        sb.append("SomeUser\n");
+        sb.append("\n");
+        sb.append("and the FMS Private Key would be: \\\\ \n");
+        sb.append("SSK@YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,AQECAAE/ \n");
+        sb.append("\n");
+        sb.append("----\n");
+        sb.append("{{{\n");
+        sb.append("<Identity>\n");
+        sb.append("   <Name><![CDATA[SomeUser]]></Name>\n");
+        sb.append("   <PublicKey>SSK@XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,AQACAAE/</PublicKey>\n");
+        sb.append("   <PrivateKey>SSK@YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY,AQECAAE/</PrivateKey>\n");
+        sb.append("   <SingleUse>false</SingleUse>\n");
+        sb.append("   <PublishTrustList>false</PublishTrustList>\n");
+        sb.append("   <PublishBoardList>false</PublishBoardList>\n");
+        sb.append("   <PublishFreesite>false</PublishFreesite>\n");
+        sb.append("</Identity>\n");
+        sb.append("}}}\n");
+        sb.append("\n");
+        return sb.toString();
+    }
 }
diff --git a/src/fniki/wiki/WikiContext.java b/src/fniki/wiki/WikiContext.java
--- a/src/fniki/wiki/WikiContext.java
+++ b/src/fniki/wiki/WikiContext.java
@@ -41,6 +41,12 @@ public interface WikiContext extends Req
     String getString(String keyName, String defaultValue);
     int getInt(String keyName, int defaultValue);
 
+    Configuration getConfiguration();
+    Configuration getDefaultConfiguration();
+
+    // throws unchecked Configuration.ConfigurationException
+    void setConfiguration(Configuration config);
+
     // throwable can be null
     void logError(String msg, Throwable throwable);
 
@@ -48,4 +54,8 @@ public interface WikiContext extends Req
     void raiseNotFound(String msg) throws NotFoundException;  // 404
     void raiseAccessDenied(String msg) throws AccessDeniedException;  // 403
     void raiseServerError(String msg) throws ServerErrorException;  // 500
+
+    // This is so we can play nice with the Freenet plugin API.
+    // Force a download to disk of data.
+    void raiseDownload(byte[] data, String filename, String mimeType) throws DownloadException;
 }
\ No newline at end of file
diff --git a/src/fniki/wiki/child/AsyncTaskContainer.java b/src/fniki/wiki/child/AsyncTaskContainer.java
--- a/src/fniki/wiki/child/AsyncTaskContainer.java
+++ b/src/fniki/wiki/child/AsyncTaskContainer.java
@@ -55,12 +55,13 @@ public abstract class AsyncTaskContainer
     protected String mExitPage = "/";
 
 
+    // IMPORTANT: See hacks in WikiContentFilter.EXCEPTIONS if this stops working.
     // 15 second refresh if the task isn't finished.
     protected String metaRefresh() {
         if (isFinished()) {
             return "";
         }
-        return "<meta http-equiv=\"refresh\" content=\"15\" />";
+        return "\n<meta http-equiv=\"refresh\" content=\"15\" />\n";
     }
 
     // DCI: make these return a string? To get rid of no return value warnings
diff --git a/src/fniki/wiki/child/SettingConfig.java b/src/fniki/wiki/child/SettingConfig.java
--- a/src/fniki/wiki/child/SettingConfig.java
+++ b/src/fniki/wiki/child/SettingConfig.java
@@ -24,30 +24,129 @@
 
 package fniki.wiki.child;
 
+import java.io.UnsupportedEncodingException;
+import static ys.wikiparser.Utils.*;
+
 import fniki.wiki.ArchiveManager;
 import fniki.wiki.ChildContainerException;
+import fniki.wiki.Configuration;
 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 final static String UTF8 = "UTF-8"; // DCI: import?
+
+    // Used by export.
+    private final static String CONFIG_NAME = "jfniki.cfg";
+    private final static String CONFIG_TYPE = "application/octet-stream";
 
     private boolean mFinished = false;
+    private Configuration mConfig;
     private String mMsg = "";
 
-    public SettingConfig(WikiApp wikiApp, ArchiveManager archiveManager) {
-        mWikiApp = wikiApp;
-        mArchiveManager = archiveManager;
+    // Doesn't validate.
+    private static Configuration parseConfigFromPost(Query query, int listenPort)  {
+        boolean allowImages = false;
+        if (query.containsKey("images")) {
+            allowImages = true;
+        }
+        int fcpPort = -1;
+        int fmsPort = -1;
+        try {
+            fcpPort = Integer.parseInt(query.get("fcpport"));
+        } catch (NumberFormatException nfe) {
+            // NOP
+        }
+
+        try {
+            fmsPort = Integer.parseInt(query.get("fmsport"));
+        } catch (NumberFormatException nfe) {
+            // NOP
+        }
+
+        Configuration config = new Configuration(listenPort,
+                                                 query.get("fcphost"),
+                                                 fcpPort,
+                                                 query.get("fpprefix"),
+                                                 allowImages,
+                                                 query.get("fmshost"),
+                                                 fmsPort,
+                                                 query.get("fmsid"),
+                                                 query.get("fmsssk"),
+                                                 query.get("fmsgroup"),
+                                                 query.get("wikiname"));
+        return config;
     }
 
     private void handlePost(WikiContext context) throws ChildContainerException {
         Query query = context.getQuery();
-        if (query.containsKey("discarded")) {
-            mMsg = "Discarded changes.";
+
+        if (query.containsKey("export")) {
+            // Pop a save-as dialog for configuration.
+            try {
+                // Save any changes the user made.
+                mConfig = parseConfigFromPost(query, context.getInt("listen_port", 8083));
+                mConfig.validate();
+
+                // Force browser to save the config file to disk.
+                context.raiseDownload(mConfig.toStringRep().getBytes(UTF8),
+                                      CONFIG_NAME, CONFIG_TYPE);
+            } catch (Configuration.ConfigurationException cfe) {
+                mMsg = "Refused to export: " + cfe.getMessage();
+                return;
+            } catch (UnsupportedEncodingException uee) {
+                mMsg = "Export failed: " + uee.getMessage();
+                return;
+            }
+        }
+
+        if (query.containsKey("import")) {
+            // Read imported config.
+            if (!query.containsKey("upload")) {
+                mMsg = "Set the file you want to import from!";
+                return;
+            }
+
+            try {
+                Configuration config = Configuration.fromStringRep(query.get("upload"));
+                config.validate();
+                mConfig = config;
+                mMsg = "Imported configuration!";
+                return;
+            } catch (Configuration.ConfigurationException cfe) {
+                mMsg = "Refused to import: " + cfe.getMessage();
+                return;
+            }
+        }
+
+        if (query.containsKey("defaults")) {
+            mConfig = context.getDefaultConfiguration();
+            try {
+                mConfig.validate();
+                mMsg = "Reset to default values."; // Won't see this because fms config not set.
+            } catch (Configuration.ConfigurationException cfe) {
+                // Handle invalid parameters;
+                mMsg = cfe.getMessage();
+                return;
+            }
+            return;
+        }
+
+        if (!query.containsKey("done")) {
+            return;
+        }
+
+        mConfig = parseConfigFromPost(query, context.getInt("listen_port", 8083));
+
+        try {
+            mConfig.validate();
+            context.setConfiguration(mConfig);
+            mMsg = "Saved configuration changes.";
+        } catch (Configuration.ConfigurationException cfe) {
+            // Handle invalid parameters;
+            mMsg = cfe.getMessage();
             return;
         }
 
@@ -58,148 +157,125 @@ public class SettingConfig implements Mo
                                            "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(),
+                             getMsgHtml(),
                              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" : "",
+                             mConfig.mFcpHost,
+                             mConfig.mFcpPort,
+                             mConfig.mFproxyPrefix,
+                             mConfig.mFmsHost,
+                             mConfig.mFmsPort,
+                             mConfig.mFmsSsk,
+                             mConfig.mFmsId,
+                             mConfig.mFmsGroup,
+                             mConfig.mWikiName,
+                             mConfig.mAllowImages ? "checked" : "",
                              // IMPORTANT: Won't work as a plugin without this.
                              context.getString("form_password", "FORM_PASSWORD_NOT_SET"));
     }
 
+    public String getMsgHtml() {
+        if (mMsg == null || mMsg.trim().equals("")) {
+            return "";
+        }
+        return String.format("<hr>%s<hr>\n", escapeHTML(mMsg));
+    }
+
     public boolean isFinished() {return mFinished; }
     public void cancel() { mFinished = true; }
     public void entered(WikiContext context) {
         mFinished = false;
+        mConfig = context.getConfiguration();
         mMsg = "";
     }
+
     public void exited() {}
 
     //////////////////////////////////////////////////
-    private static final String FORM_FIELDS[] = new String[] {
-        "fcphost", "fcpport", "fpprefix", "fmshost", "fmsport",
-        "fmsssk", "fmsid", "wikiname", "images",
-    };
 
+    // All fields used by the template must be in QueryBase.PARAMS:
+    // "fcphost", "fcpport", "fpprefix", "fmshost", "fmsport",
+    // "fmsssk", "fmsid", "wikiname", "images", "fmsbase", "formPassword", "defaults", "done"
+
+    // READ the comment above before modifiy the template!
     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");
+        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("%s\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=\"%d\" /></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=\"%d\" /> </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><input type=\"checkbox\" name=\"images\" %s/>Enable Images</td>\n");
+        sb.append("  </tr>\n");
+        sb.append("  <input type=\"hidden\" name=\"formPassword\" value=\"%s\"/>\n");
+        sb.append("</table>\n");
+        sb.append("<input name=\"defaults\" type=\"submit\" value=\"Reset Defaults\"/>\n");
+        sb.append("<input name=\"done\" type=\"submit\" value=\"Done\"/>\n");
+        sb.append("<hr>\n");
+        sb.append("<input name=\"import\" type=\"submit\" value=\"Import Configuration\"/>\n");
+        sb.append("<input type=\"file\" name=\"upload\" size=\"64\">\n");
+        sb.append("<hr>\n");
+        sb.append("<input name=\"export\" type=\"submit\" value=\"Export Configuration\"/>\n");
+        sb.append("<hr>\n");
+        sb.append("</form>\n");
+        sb.append("\n");
+        sb.append("</body>\n");
+        sb.append("</html>\n");
 
         return sb.toString();
     }
 }
 
+// DCI: include form  html and script in comments.
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
@@ -140,7 +140,13 @@ public class WikiContainer implements Ch
         if (context.getStorage().hasPage(name)) {
             buffer.append(renderXHTML(context, context.getStorage().getPage(name)));
         } else {
-            buffer.append("Page doesn't exist in the wiki yet.");
+            if (name.equals(context.getString("default_page", "Front_Page"))) {
+                buffer.append(renderXHTML(context,
+                                          context.getString("default_wikitext",
+                                                            "Page doesn't exist in the wiki yet.")));
+            } else {
+                buffer.append("Page doesn't exist in the wiki yet.");
+            }
         }
         addFooter(context, name, buffer);
         return buffer.toString();