/* A web application to read, edit and submit changes to a jfniki wiki in Freenet.
*
* Copyright (C) 2010, 2011 Darrell Karbott
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
*
* This file was developed as component of
* "fniki" (a wiki implementation running over Freenet).
*/
package fniki.wiki;
import static ys.wikiparser.Utils.*; // DCI: clean up
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import wormarc.FileManifest;
import wormarc.IOUtil;
import static fniki.wiki.HtmlUtils.*;
import static fniki.wiki.Validations.*;
import fniki.wiki.child.AsyncTaskContainer;
import fniki.wiki.child.DefaultRedirect;
import fniki.wiki.child.GotoRedirect;
import fniki.wiki.child.LoadingArchive;
import fniki.wiki.child.LoadingChangeLog;
import fniki.wiki.child.LoadingVersionList;
import fniki.wiki.child.QueryError;
import fniki.wiki.child.ResetToEmptyWiki;
import fniki.wiki.child.SettingConfig;
import fniki.wiki.child.Submitting;
import fniki.wiki.child.WikiContainer;
import fniki.freenet.filter.ContentFilterFactory;
// 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);
// Time to wait for FCP before giving up on inverting private key.
private final static int INVERT_TIMEOUT_MS = 30 * 1000;
// Delegate to implement link, image and macro handling in wikitext.
private final FreenetWikiTextParser.ParserDelegate mParserDelegate;
// ChildContainers for non-modal UI elements.
private final ChildContainer mDefaultRedirect;
private final ChildContainer mGotoRedirect;
private final ChildContainer mQueryError;
private final ChildContainer mWikiContainer;
private final ChildContainer mResetToEmptyWiki;
// ChildContainers for modal UI states.
private final ChildContainer mSettingConfig;
private final ChildContainer mLoadingVersionList;
private final ChildContainer mLoadingArchive;
private final ChildContainer mSubmitting;
private final ChildContainer mLoadingChangeLog;
// The current default UI state.
private ChildContainer mState;
// Transient, per request state.
private Request mRequest;
private ArchiveManager mArchiveManager;
// Belt and braces. Run the ContentFilter from the Freenet fred codebase
// over all output before serving it.
private ContentFilter mFilter;
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() {
mFilter = ContentFilterFactory.create(mFproxyPrefix, containerPrefix());
}
public WikiApp(ArchiveManager archiveManager) {
mParserDelegate = new LocalParserDelegate(this, archiveManager);
mDefaultRedirect = new DefaultRedirect();
mGotoRedirect = new GotoRedirect();
mQueryError = new QueryError();
mWikiContainer = new WikiContainer();
mResetToEmptyWiki = new ResetToEmptyWiki(archiveManager);
mSettingConfig = new SettingConfig();
mLoadingVersionList = new LoadingVersionList(archiveManager);
mLoadingArchive = new LoadingArchive(archiveManager);
mSubmitting = new Submitting(archiveManager);
mLoadingChangeLog = new LoadingChangeLog(archiveManager);
mState = mWikiContainer;
mArchiveManager = archiveManager;
resetContentFilter();
}
public void setFproxyPrefix(String value) {
if (!value.startsWith("http") && value.endsWith("/")) {
throw new IllegalArgumentException("Expected a value starting with 'http' and ending with '/'");
}
mFproxyPrefix = value;
resetContentFilter();
}
public void setAllowImages(boolean value) {
mAllowImages = value;
}
public void setFormPassword(String value) {
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;
}
System.err.println(String.format("[%s] => [%s]",
mState.getClass().getName(),
container.getClass().getName()));
if (mState != null && mState instanceof ModalContainer) {
((ModalContainer)mState).exited();
}
mState = container;
if (mState instanceof ModalContainer) {
((ModalContainer)mState).entered(context);
}
return mState;
}
// This function defines the UI state machine.
private ChildContainer routeRequest(WikiContext request)
throws IOException {
// Fail immediately if there are problems in the glue code.
if (request.getPath() == null) {
throw new RuntimeException("Assertion Failure: path == null");
}
if (request.getQuery() == null) {
throw new RuntimeException("Assertion Failure: query == null");
}
if (request.getAction() == null) {
throw new RuntimeException("Assertion Failure: action == null");
}
if (request.getTitle() == null) {
throw new RuntimeException("Assertion Failure: title == null");
}
String action = request.getAction();
if (mState instanceof ModalContainer) {
// Handle transitions out of modal UI states.
ModalContainer state = (ModalContainer)mState;
if (action.equals("finished")) {
System.err.println("finished");
if (!state.isFinished()) {
System.err.println("canceling");
state.cancel();
try {
Thread.sleep(250); // HACK
}
catch (InterruptedException ie) {
}
}
// No "else" because it might have finished while sleeping.
if (state.isFinished()) {
System.err.println("finished");
setState(request, mWikiContainer);
return mGotoRedirect;
}
}
return state; // Don't leave the modal UI state until finished.
}
String path = request.getPath();
int slashCount = 0;
for (int index = 0; index < path.length(); index++) {
if (path.charAt(index) == '/') {
slashCount++;
}
}
// 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);
} else if (path.equals("fniki/submit")) {
return setState(request, mSubmitting);
} else if (path.equals("fniki/changelog")) {
return setState(request, mLoadingChangeLog);
} else if (path.equals("fniki/getversions")) {
return setState(request, mLoadingVersionList);
} else if (path.equals("fniki/loadarchive")) {
return setState(request, mLoadingArchive);
} else if (path.equals("fniki/resettoempty")) {
return setState(request, mResetToEmptyWiki);
} else if (path.equals("")) {
return mDefaultRedirect;
} else if (slashCount != 0) {
return mQueryError;
} else {
setState(request, mWikiContainer);
}
return mState;
}
// All requests are serialized! Hmmmm....
public synchronized String handle(WikiContext context) throws ChildContainerException {
try {
ChildContainer childContainer = routeRequest(context);
System.err.println("Request routed to: " + childContainer.getClass().getName());
return mFilter.filter(childContainer.handle(context));
} catch (ChildContainerException cce) {
// Normal, used to do redirection.
throw cce;
} catch (Exception e) {
context.logError("WikiApp.handle -- untrapped!:", e);
throw new ServerErrorException("Coding error. Sorry :-(");
}
}
////////////////////////////////////////////////////////////
private static class LocalParserDelegate extends WikiParserDelegate {
final WikiContext mContext;
LocalParserDelegate(WikiContext context, ArchiveManager archiveManager) {
super(archiveManager);
mContext = context;
}
// Implement base class abstract methods to supply the functionality
// specific to live wikis, mostly by delegating to the WikiContext.
protected String getContainerPrefix() {
return containerPrefix(); // LATER: revisit.
}
protected boolean getFreenetLinksAllowed(){
return mContext.getString("fproxy_prefix", null) != null;
}
protected boolean getImagesAllowed() {
return mContext.getInt("allow_images", 0) != 0;
}
protected String makeLink(String containerRelativePath) {
return mContext.makeLink(containerRelativePath);
}
protected String makeFreenetLink(String uri) {
String prefix = mContext.getString("fproxy_prefix", null);
if (prefix == null) {
throw new RuntimeException("fproxy_prefix is null!");
}
return makeFproxyHref(prefix, uri);
}
}
// Hmmmm... kind of weird. I can't remember why I used this static method instead of a constant.
// NO trailing slash.
private static String containerPrefix() { return "/plugins/fniki.freenet.plugin.Fniki"; }
////////////////////////////////////////////////////////////
// Wiki context implementations.
public WikiTextStorage getStorage() throws IOException { return mArchiveManager.getStorage(); }
public FreenetWikiTextParser.ParserDelegate getParserDelegate() { return mParserDelegate; }
public String getString(String keyName, String defaultValue) {
if (keyName.equals("default_page")) {
return "Front_Page";
} else if (keyName.equals("fproxy_prefix")) {
if (mFproxyPrefix == null) {
return defaultValue;
}
return mFproxyPrefix;
} else if (keyName.equals("parent_uri")) {
if (mArchiveManager.getParentUri() == null) {
// Can be null
return defaultValue;
}
return mArchiveManager.getParentUri();
} else if (keyName.equals("container_prefix")) {
return containerPrefix();
} else if (keyName.equals("form_password") && mFormPassword != null) {
return mFormPassword;
} else if (keyName.equals("default_wikitext")) {
return getDefaultWikiText();
} else if (keyName.equals("wikiname")) {
if (mArchiveManager.getBissName() != null) {
return mArchiveManager.getBissName();
}
} else if (keyName.equals("fms_group")) {
if (mArchiveManager.getFmsGroup() != null) {
return mArchiveManager.getFmsGroup();
}
}
return defaultValue;
}
public int getInt(String keyName, int defaultValue) {
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),
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; }
public String getPublicFmsId(String fmsId, String privateSSK) {
if (fmsId == null || privateSSK == null || fmsId.indexOf("@") != -1) {
return "???";
}
try {
try {
String publicKey = mArchiveManager.invertPrivateSSK(privateSSK, INVERT_TIMEOUT_MS);
int pos = publicKey.indexOf(",");
if (pos == -1 || pos < 5) {
return "???";
}
return fmsId + publicKey.substring("SSK".length(), pos);
} catch (IllegalArgumentException iae) {
// Was called with an invalid privateSSK value
return "???";
}
} catch (IOException ioe) {
logError("getPublicFmsId failed", ioe);
return "???";
}
}
// 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);
}
public String makeLink(String containerRelativePath) {
// Hacks to find bugs
if (!containerRelativePath.startsWith("/")) {
containerRelativePath = "/" + containerRelativePath;
System.err.println("WikiApp.makeLink -- added leading '/': " +
containerRelativePath);
(new RuntimeException("find missing /")).printStackTrace();
}
String full = containerPrefix() + containerRelativePath;
while (full.indexOf("//") != -1) {
System.err.println("WikiApp.makeLink -- fixing '//': " +
full);
full = full.replace("//", "/");
(new RuntimeException("find extra /")).printStackTrace();
}
return full;
}
public void raiseRedirect(String toLocation, String msg) throws RedirectException {
throw new RedirectException(toLocation, msg);
}
public void raiseNotFound(String msg) throws NotFoundException {
throw new NotFoundException(msg);
}
public void raiseAccessDenied(String msg) throws AccessDeniedException {
throw new AccessDeniedException(msg);
}
public void raiseServerError(String msg) throws ServerErrorException {
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";
}
if (t == null) {
t = new RuntimeException("FAKE EXCEPTION: logError called with t == null!");
}
System.err.println("Unexpected error: " + msg + " : " + t.toString());
t.printStackTrace();
}
// Delegate to the mRequest helper instance set with setRequest().
public String getPath() { return mRequest.getPath(); }
public Query getQuery() { return mRequest.getQuery(); }
public String getAction() { return mRequest.getQuery().get("action"); }
public String getTitle() { return mRequest.getQuery().get("title"); }
////////////////////////////////////////////////////////////
public void setRequest(Request request) {
if (request == null) {
throw new IllegalArgumentException("request == null");
}
mRequest = request;
}
private static String getDefaultWikiText() {
try {
return IOUtil.readUtf8StringAndClose(WikiApp.class.getResourceAsStream("/quickstart.txt"));
} catch (IOException ioe) {
return "Couldn't load default wikitext from jar???";
}
}
}