Merge branch 'fcp-interface' into next
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 11 May 2011 04:12:43 +0000 (06:12 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 11 May 2011 04:12:43 +0000 (06:12 +0200)
This fixes #21.

1  2 
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/templates/options.html

@@@ -25,9 -25,6 +25,9 @@@ import java.util.HashSet
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
 +import java.util.Map.Entry;
 +import java.util.concurrent.ExecutorService;
 +import java.util.concurrent.Executors;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
@@@ -40,6 -37,8 +40,8 @@@ import net.pterodactylus.sone.data.Prof
  import net.pterodactylus.sone.data.Profile.Field;
  import net.pterodactylus.sone.data.Reply;
  import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.fcp.FcpInterface;
+ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
  import net.pterodactylus.sone.freenet.wot.Identity;
  import net.pterodactylus.sone.freenet.wot.IdentityListener;
  import net.pterodactylus.sone.freenet.wot.IdentityManager;
@@@ -51,7 -50,6 +53,7 @@@ import net.pterodactylus.util.config.Co
  import net.pterodactylus.util.config.ConfigurationException;
  import net.pterodactylus.util.logging.Logging;
  import net.pterodactylus.util.number.Numbers;
 +import net.pterodactylus.util.validation.IntegerRangeValidator;
  import net.pterodactylus.util.validation.Validation;
  import net.pterodactylus.util.version.Version;
  import freenet.keys.FreenetURI;
@@@ -110,12 -108,12 +112,15 @@@ public class Core implements IdentityLi
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
  
 +      /** Sone downloader thread-pool. */
 +      private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
 +
        /** The update checker. */
        private final UpdateChecker updateChecker;
  
+       /** The FCP interface. */
+       private volatile FcpInterface fcpInterface;
        /** Whether the core has been stopped. */
        private volatile boolean stopped;
  
        }
  
        /**
+        * Sets the FCP interface to use.
+        *
+        * @param fcpInterface
+        *            The FCP interface to use
+        */
+       public void setFcpInterface(FcpInterface fcpInterface) {
+               this.fcpInterface = fcpInterface;
+       }
+       /**
         * Returns the status of the given Sone.
         *
         * @param sone
                        return null;
                }
                Sone sone = addLocalSone(ownIdentity);
 +              sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
 +              sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
 +              saveSone(sone);
                return sone;
        }
  
                        remoteSones.put(identity.getId(), sone);
                        soneDownloader.addSone(sone);
                        setSoneStatus(sone, SoneStatus.unknown);
 -                      new Thread(new Runnable() {
 +                      soneDownloaders.execute(new Runnable() {
  
                                @Override
                                @SuppressWarnings("synthetic-access")
                                public void run() {
 -                                      soneDownloader.fetchSone(sone);
 +                                      soneDownloader.fetchSone(sone, sone.getRequestUri());
                                }
  
 -                      }, "Sone Downloader").start();
 +                      });
                        return sone;
                }
        }
                        return;
                }
  
 +              /* initialize options. */
 +              sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
 +
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
                Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
                }
  
                /* load options. */
 -              sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
  
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (posts) {
                        posts.remove(post.getId());
                }
 +              coreListenerManager.firePostRemoved(post);
                synchronized (newPosts) {
                        markPostKnown(post);
                        knownPosts.remove(post.getId());
                        configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
                        configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
 +                      configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
                        configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
+                       configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
+                       configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
                        configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
                        configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
                        configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
        @SuppressWarnings("unchecked")
        private void loadConfiguration() {
                /* create options. */
 -              options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
 +              options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
  
                        @Override
                        public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
                        }
  
                }));
 -              options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(25));
 -              options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
 -              options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
 +              options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
 +              options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
 +              options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
 +              options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
                options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
+               options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
+                               fcpInterface.setActive(newValue);
+                       }
+               }));
+               options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
+                               fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
+                       }
+               }));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
                        return;
                }
  
 -              options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
 -              options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
 -              options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
 -              options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
 +              loadConfigurationValue("InsertionDelay");
 +              loadConfigurationValue("PostsPerPage");
 +              options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
 +              loadConfigurationValue("PositiveTrust");
 +              loadConfigurationValue("NegativeTrust");
                options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
+               options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
+               options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
                options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
  
                /* load known Sones. */
        }
  
        /**
 +       * Loads an {@link Integer} configuration value for the option with the
 +       * given name, logging validation failures.
 +       *
 +       * @param optionName
 +       *            The name of the option to load
 +       */
 +      private void loadConfigurationValue(String optionName) {
 +              try {
 +                      options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
 +              } catch (IllegalArgumentException iae1) {
 +                      logger.log(Level.WARNING, "Invalid value for " + optionName + " in configuration, using default.");
 +              }
 +      }
 +
 +      /**
         * Generate a Sone URI from the given URI and latest edition.
         *
         * @param uriString
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId());
                                sone.setIdentity(identity);
 +                              sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
                                soneDownloader.addSone(sone);
                                soneDownloader.fetchSone(sone);
                        }
        @Override
        public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
                trustedIdentities.get(ownIdentity).remove(identity);
 +              boolean foundIdentity = false;
 +              for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
 +                      if (trustedIdentity.getKey().equals(ownIdentity)) {
 +                              continue;
 +                      }
 +                      if (trustedIdentity.getValue().contains(identity)) {
 +                              foundIdentity = true;
 +                      }
 +              }
 +              if (foundIdentity) {
 +                      /* some local identity still trusts this identity, don’t remove. */
 +                      return;
 +              }
 +              Sone sone = getSone(identity.getId(), false);
 +              if (sone == null) {
 +                      /* TODO - we don’t have the Sone anymore. should this happen? */
 +                      return;
 +              }
 +              synchronized (posts) {
 +                      synchronized (newPosts) {
 +                              for (Post post : sone.getPosts()) {
 +                                      posts.remove(post.getId());
 +                                      newPosts.remove(post.getId());
 +                                      coreListenerManager.firePostRemoved(post);
 +                              }
 +                      }
 +              }
 +              synchronized (replies) {
 +                      synchronized (newReplies) {
 +                              for (Reply reply : sone.getReplies()) {
 +                                      replies.remove(reply.getId());
 +                                      newReplies.remove(reply.getId());
 +                                      coreListenerManager.fireReplyRemoved(reply);
 +                              }
 +                      }
 +              }
 +              synchronized (remoteSones) {
 +                      remoteSones.remove(identity.getId());
 +              }
 +              synchronized (newSones) {
 +                      newSones.remove(identity.getId());
 +              }
        }
  
        //
                }
  
                /**
 +               * Validates the given insertion delay.
 +               *
 +               * @param insertionDelay
 +               *            The insertion delay to validate
 +               * @return {@code true} if the given insertion delay was valid, {@code
 +               *         false} otherwise
 +               */
 +              public boolean validateInsertionDelay(Integer insertionDelay) {
 +                      return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
 +              }
 +
 +              /**
                 * Sets the insertion delay
                 *
                 * @param insertionDelay
                }
  
                /**
 +               * Validates the number of posts per page.
 +               *
 +               * @param postsPerPage
 +               *            The number of posts per page
 +               * @return {@code true} if the number of posts per page was valid,
 +               *         {@code false} otherwise
 +               */
 +              public boolean validatePostsPerPage(Integer postsPerPage) {
 +                      return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
 +              }
 +
 +              /**
                 * Sets the number of posts to show per page.
                 *
                 * @param postsPerPage
                }
  
                /**
 +               * Returns whether Sone requires full access to be even visible.
 +               *
 +               * @return {@code true} if Sone requires full access, {@code false}
 +               *         otherwise
 +               */
 +              public boolean isRequireFullAccess() {
 +                      return options.getBooleanOption("RequireFullAccess").get();
 +              }
 +
 +              /**
 +               * Sets whether Sone requires full access to be even visible.
 +               *
 +               * @param requireFullAccess
 +               *            {@code true} if Sone requires full access, {@code false}
 +               *            otherwise
 +               */
 +              public void setRequireFullAccess(Boolean requireFullAccess) {
 +                      options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
 +              }
 +
 +              /**
                 * Returns the positive trust.
                 *
                 * @return The positive trust
                }
  
                /**
 +               * Validates the positive trust.
 +               *
 +               * @param positiveTrust
 +               *            The positive trust to validate
 +               * @return {@code true} if the positive trust was valid, {@code false}
 +               *         otherwise
 +               */
 +              public boolean validatePositiveTrust(Integer positiveTrust) {
 +                      return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
 +              }
 +
 +              /**
                 * Sets the positive trust.
                 *
                 * @param positiveTrust
                }
  
                /**
 +               * Validates the negative trust.
 +               *
 +               * @param negativeTrust
 +               *            The negative trust to validate
 +               * @return {@code true} if the negative trust was valid, {@code false}
 +               *         otherwise
 +               */
 +              public boolean validateNegativeTrust(Integer negativeTrust) {
 +                      return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
 +              }
 +
 +              /**
                 * Sets the negative trust.
                 *
                 * @param negativeTrust
                }
  
                /**
+                * Returns whether the {@link FcpInterface FCP interface} is currently
+                * active.
+                *
+                * @see FcpInterface#setActive(boolean)
+                * @return {@code true} if the FCP interface is currently active,
+                *         {@code false} otherwise
+                */
+               public boolean isFcpInterfaceActive() {
+                       return options.getBooleanOption("ActivateFcpInterface").get();
+               }
+               /**
+                * Sets whether the {@link FcpInterface FCP interface} is currently
+                * active.
+                *
+                * @see FcpInterface#setActive(boolean)
+                * @param fcpInterfaceActive
+                *            {@code true} to activate the FCP interface, {@code false}
+                *            to deactivate the FCP interface
+                * @return This preferences object
+                */
+               public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
+                       options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+                       return this;
+               }
+               /**
+                * Returns the action level for which full access to the FCP interface
+                * is required.
+                *
+                * @return The action level for which full access to the FCP interface
+                *         is required
+                */
+               public FullAccessRequired getFcpFullAccessRequired() {
+                       return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+               }
+               /**
+                * Sets the action level for which full access to the FCP interface is
+                * required
+                *
+                * @param fcpFullAccessRequired
+                *            The action level
+                * @return This preferences
+                */
+               public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+                       options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
+                       return this;
+               }
+               /**
                 * Returns whether the rescue mode is active.
                 *
                 * @return {@code true} if the rescue mode is active, {@code false}
@@@ -1,5 -1,5 +1,5 @@@
  /*
 - * FreenetSone - SonePlugin.java - Copyright © 2010 David Roden
 + * Sone - SonePlugin.java - Copyright © 2010 David Roden
   *
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
@@@ -24,6 -24,7 +24,7 @@@ import java.util.logging.Logger
  
  import net.pterodactylus.sone.core.Core;
  import net.pterodactylus.sone.core.FreenetInterface;
+ import net.pterodactylus.sone.fcp.FcpInterface;
  import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
  import net.pterodactylus.sone.freenet.plugin.PluginConnector;
  import net.pterodactylus.sone.freenet.wot.IdentityManager;
@@@ -40,10 -41,14 +41,14 @@@ import freenet.l10n.BaseL10n.LANGUAGE
  import freenet.l10n.PluginL10n;
  import freenet.pluginmanager.FredPlugin;
  import freenet.pluginmanager.FredPluginBaseL10n;
+ import freenet.pluginmanager.FredPluginFCP;
  import freenet.pluginmanager.FredPluginL10n;
  import freenet.pluginmanager.FredPluginThreadless;
  import freenet.pluginmanager.FredPluginVersioned;
+ import freenet.pluginmanager.PluginReplySender;
  import freenet.pluginmanager.PluginRespirator;
+ import freenet.support.SimpleFieldSet;
+ import freenet.support.api.Bucket;
  
  /**
   * This class interfaces with Freenet. It is the class that is loaded by the
@@@ -51,7 -56,7 +56,7 @@@
   *
   * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
   */
- public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
+ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
  
        static {
                /* initialize logging. */
@@@ -78,7 -83,7 +83,7 @@@
        }
  
        /** The version. */
 -      public static final Version VERSION = new Version(0, 6);
 +      public static final Version VERSION = new Version(0, 6, 4);
  
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
@@@ -92,6 -97,9 +97,9 @@@
        /** The web interface. */
        private WebInterface webInterface;
  
+       /** The FCP interface. */
+       private FcpInterface fcpInterface;
        /** The l10n helper. */
        private PluginL10n l10n;
  
                        webInterface = new WebInterface(this);
                        core.addCoreListener(webInterface);
  
+                       /* create FCP interface. */
+                       fcpInterface = new FcpInterface(core);
+                       core.setFcpInterface(fcpInterface);
                        /* create the identity manager. */
                        identityManager.addIdentityListener(core);
  
        }
  
        //
+       // INTERFACE FredPluginFCP
+       //
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+               fcpInterface.handle(pluginReplySender, parameters, data, accessType);
+       }
+       //
        // INTERFACE FredPluginL10n
        //
  
  
  package net.pterodactylus.sone.web;
  
 +import java.util.ArrayList;
 +import java.util.List;
 +
  import net.pterodactylus.sone.core.Core.Preferences;
  import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
  import net.pterodactylus.sone.web.page.Page.Request.Method;
  import net.pterodactylus.util.number.Numbers;
  import net.pterodactylus.util.template.Template;
@@@ -59,43 -57,29 +60,48 @@@ public class OptionsPage extends SoneTe
                Preferences preferences = webInterface.getCore().getPreferences();
                Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
                if (request.getMethod() == Method.POST) {
 +                      List<String> fieldErrors = new ArrayList<String>();
                        if (currentSone != null) {
                                boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
                                currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
                                webInterface.getCore().saveSone(currentSone);
                        }
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
 -                      preferences.setInsertionDelay(insertionDelay);
 +                      if (!preferences.validateInsertionDelay(insertionDelay)) {
 +                              fieldErrors.add("insertion-delay");
 +                      } else {
 +                              preferences.setInsertionDelay(insertionDelay);
 +                      }
                        Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
 -                      preferences.setPostsPerPage(postsPerPage);
 +                      if (!preferences.validatePostsPerPage(postsPerPage)) {
 +                              fieldErrors.add("posts-per-page");
 +                      } else {
 +                              preferences.setPostsPerPage(postsPerPage);
 +                      }
 +                      boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
 +                      preferences.setRequireFullAccess(requireFullAccess);
                        Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
 -                      preferences.setPositiveTrust(positiveTrust);
 +                      if (!preferences.validatePositiveTrust(positiveTrust)) {
 +                              fieldErrors.add("positive-trust");
 +                      } else {
 +                              preferences.setPositiveTrust(positiveTrust);
 +                      }
                        Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
 -                      preferences.setNegativeTrust(negativeTrust);
 +                      if (!preferences.validateNegativeTrust(negativeTrust)) {
 +                              fieldErrors.add("negative-trust");
 +                      } else {
 +                              preferences.setNegativeTrust(negativeTrust);
 +                      }
                        String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
                        if (trustComment.trim().length() == 0) {
                                trustComment = null;
                        }
                        preferences.setTrustComment(trustComment);
+                       boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active");
+                       preferences.setFcpInterfaceActive(fcpInterfaceActive);
+                       Integer fcpFullAccessRequiredInteger = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
+                       FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
+                       preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
                        boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
                        preferences.setSoneRescueMode(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
                        preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
                        webInterface.getCore().saveConfiguration();
 -                      throw new RedirectException(getPath());
 +                      if (fieldErrors.isEmpty()) {
 +                              throw new RedirectException(getPath());
 +                      }
 +                      templateContext.set("fieldErrors", fieldErrors);
                }
                if (currentSone != null) {
                        templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
                }
                templateContext.set("insertion-delay", preferences.getInsertionDelay());
                templateContext.set("posts-per-page", preferences.getPostsPerPage());
 +              templateContext.set("require-full-access", preferences.isRequireFullAccess());
                templateContext.set("positive-trust", preferences.getPositiveTrust());
                templateContext.set("negative-trust", preferences.getNegativeTrust());
                templateContext.set("trust-comment", preferences.getTrustComment());
+               templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
+               templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
                templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
                templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
                templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
@@@ -33,24 -33,25 +33,30 @@@ Page.Options.Page.Title=Option
  Page.Options.Page.Description=These options influence the runtime behaviour of the Sone plugin.
  Page.Options.Section.SoneSpecificOptions.Title=Sone-specific Options
  Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only available if you are {link}logged in{/link}.
 -Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically.
 +Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
 +Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically. Note that this will only follow Sones that are discovered after you activate this option!
  Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
  Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
  Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
 +Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
  Page.Options.Section.TrustOptions.Title=Trust Settings
  Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
  Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
  Page.Options.Option.TrustComment.Description=The comment that will be set in the web of trust for any trust you assign from Sone.
+ Page.Options.Section.FcpOptions.Title=FCP Interface Settings
+ Page.Options.Option.FcpInterfaceActive.Description=Activate the FCP interface to allow other plugins and remote clients to access your Sone plugin.
+ Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection from allowed hosts (see your {link}node’s configuration, section “FCP”{/link})
+ Page.Options.Option.FcpFullAccessRequired.Value.No=No
+ Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
+ Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
  Page.Options.Section.RescueOptions.Title=Rescue Settings
 -Page.Options.Option.SoneRescueMode.Description=Try to rescue your Sones at the next start of the Sone plugin. This will read your all your old Sones from Freenet and ignore any disappearing postings and replies. You have to unlock your local Sones after they have been restored and you have to manually disable the rescue mode once you are satisfied with what has been restored!
 +Page.Options.Option.SoneRescueMode.Description1=Try to rescue your Sones at the next start of the Sone plugin. The Rescue Mode will start at the latest known edition and will try to download all editions sequentially backwards, merging all discovered posts and replies together, until it is stopped or it has reached the first edition.
 +Page.Options.Option.SoneRescueMode.Description2=When using the Rescue Mode because Sone lost its configuration it usually suffices to let the Rescue Mode only run for a short time; use a second tab to control how many posts of your Sone are visible again. As soon as the last valid edition is loaded you can then deactivate the Rescue Mode.
 +Page.Options.Option.SoneRescueMode.Description3=Note that when you use the Rescue Mode posts that you have deleted after they have been inserted will have to be deleted again. Unfortunately this is an unavoidable side effect of the Rescue Mode.
  Page.Options.Section.Cleaning.Title=Clean Up
  Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
  Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
 +Page.Options.Warnings.ValueNotChanged=This option was not changed because value you specified was not valid.
  Page.Options.Button.Save=Save
  
  Page.Login.Title=Login - Sone
@@@ -138,8 -139,7 +144,8 @@@ Page.ViewSone.PostList.Title=Posts by {
  Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
  Page.ViewSone.Profile.Title=Profile
  Page.ViewSone.Profile.Label.Name=Name
 -Page.ViewSone.Replies.Title=Replies to Posts
 +Page.ViewSone.Profile.Name.WoTLink=Web of trust profile
 +Page.ViewSone.Replies.Title=Posts {sone} has replied to
  
  Page.ViewPost.Title=View Post - Sone
  Page.ViewPost.Page.Title=View Post by {sone}
@@@ -229,8 -229,6 +235,8 @@@ View.Sone.Status.Downloading=This Sone 
  View.Sone.Status.Inserting=This Sone is currently being inserted.
  
  View.Post.UnknownAuthor=(unknown)
 +View.Post.Permalink=link post
 +View.Post.PermalinkAuthor=link author
  View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
  View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
  View.Post.DeleteLink=Delete
@@@ -258,7 -256,7 +264,7 @@@ View.Time.XHoursAgo=${hour} hours ag
  View.Time.ADayAgo=about a day ago
  View.Time.XDaysAgo=${day} days ago
  View.Time.AWeekAgo=about a week ago
 -View.Time.XWeeksAgo=${week} week ago
 +View.Time.XWeeksAgo=${week} weeks ago
  View.Time.AMonthAgo=about a month ago
  View.Time.XMonthsAgo=${month} months ago
  View.Time.AYearAgo=about a year ago
@@@ -31,8 -31,6 +31,8 @@@
  
                <%ifnull currentSone>
                        <p><%= Page.Options.Section.SoneSpecificOptions.NotLoggedIn|l10n|html|replace needle="{link}" replacement='<a href="login.html">'|replace needle="{/link}" replacement='</a>'></p>
 +              <%else>
 +                      <p><%= Page.Options.Section.SoneSpecificOptions.LoggedIn|l10n|html></p>
                <%/if>
  
                <p>
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
  
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
 +              <%if =insertion-delay|in collection=fieldErrors>
 +                      <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
 +              <%/if>
                <p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
  
                <p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
 +              <%if =posts-per-page|in collection=fieldErrors>
 +                      <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
 +              <%/if>
                <p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
  
 +              <p>
 +                      <input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
 +                      <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
 +              </p>
 +
                <h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
  
                <p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
 +              <%if =positive-trust|in collection=fieldErrors>
 +                      <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
 +              <%/if>
                <p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
  
                <p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
 +              <%if =negative-trust|in collection=fieldErrors>
 +                      <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
 +              <%/if>
                <p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
  
                <p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
                <p><input type="text" name="trust-comment" value="<% trust-comment|html>" /></p>
  
+               <h2><%= Page.Options.Section.FcpOptions.Title|l10n|html></h2>
+               <p><input type="checkbox" name="fcp-interface-active"<%if fcp-interface-active> checked="checked"<%/if> /> <%= Page.Options.Option.FcpInterfaceActive.Description|l10n|html></p>
+               <p>
+                       <%= Page.Options.Option.FcpFullAccessRequired.Description|l10n|html|replace needle="{link}" replacement='<a href="/config/fcp">'|replace needle="{/link}" replacement='</a>'>
+                       <select name="fcp-full-access-required">
+                               <option value="0"<%if fcp-full-access-required|match value="0"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.No|l10n|html></option>
+                               <option value="1"<%if fcp-full-access-required|match value="1"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Writing|l10n|html></option>
+                               <option value="2"<%if fcp-full-access-required|match value="2"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Always|l10n|html></option>
+                       </select>
+               </p>
                <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
  
 -              <p><%= Page.Options.Option.SoneRescueMode.Description|l10n|html></p>
 +              <p><%= Page.Options.Option.SoneRescueMode.Description1|l10n|html></p>
 +              <p><%= Page.Options.Option.SoneRescueMode.Description2|l10n|html></p>
 +              <p><%= Page.Options.Option.SoneRescueMode.Description3|l10n|html></p>
                <p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
  
                <h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>