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();