Merge branch 'edit-wot-trust' into next
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 15 Jan 2011 20:30:26 +0000 (21:30 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 15 Jan 2011 20:30:26 +0000 (21:30 +0100)
Conflicts:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java

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/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/TrustPage.java
src/main/java/net/pterodactylus/sone/web/UntrustPage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js

@@@ -34,19 -34,20 +34,22 @@@ import net.pterodactylus.sone.core.Opti
  import net.pterodactylus.sone.data.Client;
  import net.pterodactylus.sone.data.Post;
  import net.pterodactylus.sone.data.Profile;
 +import net.pterodactylus.sone.data.Profile.Field;
  import net.pterodactylus.sone.data.Reply;
  import net.pterodactylus.sone.data.Sone;
  import net.pterodactylus.sone.freenet.wot.Identity;
  import net.pterodactylus.sone.freenet.wot.IdentityListener;
  import net.pterodactylus.sone.freenet.wot.IdentityManager;
  import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+ import net.pterodactylus.sone.freenet.wot.Trust;
+ import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
  import net.pterodactylus.sone.main.SonePlugin;
  import net.pterodactylus.util.config.Configuration;
  import net.pterodactylus.util.config.ConfigurationException;
  import net.pterodactylus.util.logging.Logging;
  import net.pterodactylus.util.number.Numbers;
+ import net.pterodactylus.util.validation.Validation;
 +import net.pterodactylus.util.version.Version;
  import freenet.keys.FreenetURI;
  
  /**
@@@ -54,7 -55,7 +57,7 @@@
   *
   * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
   */
 -public class Core implements IdentityListener {
 +public class Core implements IdentityListener, UpdateListener {
  
        /**
         * Enumeration for the possible states of a {@link Sone}.
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
  
 +      /** The update checker. */
 +      private final UpdateChecker updateChecker;
 +
        /** Whether the core has been stopped. */
        private volatile boolean stopped;
  
        /** All known replies. */
        private Set<String> knownReplies = new HashSet<String>();
  
+       /** Trusted identities, sorted by own identities. */
+       private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
        /**
         * Creates a new core.
         *
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
 +              this.updateChecker = new UpdateChecker(freenetInterface);
        }
  
        //
        }
  
        /**
 +       * Returns the update checker.
 +       *
 +       * @return The update checker
 +       */
 +      public UpdateChecker getUpdateChecker() {
 +              return updateChecker;
 +      }
 +
 +      /**
         * Returns the status of the given Sone.
         *
         * @param sone
        }
  
        /**
+        * Returns whether the target Sone is trusted by the origin Sone.
+        *
+        * @param origin
+        *            The origin Sone
+        * @param target
+        *            The target Sone
+        * @return {@code true} if the target Sone is trusted by the origin Sone
+        */
+       public boolean isSoneTrusted(Sone origin, Sone target) {
+               return trustedIdentities.containsKey(origin) && trustedIdentities.get(origin.getIdentity()).contains(target);
+       }
+       /**
         * Returns the post with the given ID.
         *
         * @param postId
         * @return The created Sone
         */
        public Sone createSone(OwnIdentity ownIdentity) {
-               identityManager.addContext(ownIdentity, "Sone");
+               try {
+                       ownIdentity.addContext("Sone");
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.SEVERE, "Could not add “Sone” context to own identity: " + ownIdentity, wote1);
+                       return null;
+               }
                Sone sone = addLocalSone(ownIdentity);
                return sone;
        }
        }
  
        /**
+        * Retrieves the trust relationship from the origin to the target. If the
+        * trust relationship can not be retrieved, {@code null} is returned.
+        *
+        * @see Identity#getTrust(OwnIdentity)
+        * @param origin
+        *            The origin of the trust tree
+        * @param target
+        *            The target of the trust
+        * @return The trust relationship
+        */
+       public Trust getTrust(Sone origin, Sone target) {
+               if (!isLocalSone(origin)) {
+                       logger.log(Level.WARNING, "Tried to get trust from remote Sone: %s", origin);
+                       return null;
+               }
+               return target.getIdentity().getTrust((OwnIdentity) origin.getIdentity());
+       }
+       /**
+        * Sets the trust value of the given origin Sone for the target Sone.
+        *
+        * @param origin
+        *            The origin Sone
+        * @param target
+        *            The target Sone
+        * @param trustValue
+        *            The trust value (from {@code -100} to {@code 100})
+        */
+       public void setTrust(Sone origin, Sone target, int trustValue) {
+               Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
+               try {
+                       ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, options.getStringOption("TrustComment").get());
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set trust for Sone: " + target, wote1);
+               }
+       }
+       /**
+        * Removes any trust assignment for the given target Sone.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void removeTrust(Sone origin, Sone target) {
+               Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
+               try {
+                       ((OwnIdentity) origin.getIdentity()).removeTrust(target.getIdentity());
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not remove trust for Sone: " + target, wote1);
+               }
+       }
+       /**
+        * Assigns the configured positive trust value for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void trustSone(Sone origin, Sone target) {
+               setTrust(origin, target, options.getIntegerOption("PositiveTrust").get());
+       }
+       /**
+        * Assigns the configured negative trust value for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void distrustSone(Sone origin, Sone target) {
+               setTrust(origin, target, options.getIntegerOption("NegativeTrust").get());
+       }
+       /**
+        * Removes the trust assignment for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void untrustSone(Sone origin, Sone target) {
+               removeTrust(origin, target);
+       }
+       /**
         * Updates the stores Sone with the given Sone.
         *
         * @param sone
                        localSones.remove(sone.getId());
                        soneInserters.remove(sone).stop();
                }
-               identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
-               identityManager.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
+               try {
+                       ((OwnIdentity) sone.getIdentity()).removeContext("Sone");
+                       ((OwnIdentity) sone.getIdentity()).removeProperty("Sone.LatestEdition");
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not remove context and properties from Sone: " + sone, wote1);
+               }
                try {
                        configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
                } catch (ConfigurationException ce1) {
                profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
                profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
  
 +              /* load profile fields. */
 +              while (true) {
 +                      String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
 +                      String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
 +                      if (fieldName == null) {
 +                              break;
 +                      }
 +                      String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
 +                      profile.addField(fieldName).setValue(fieldValue);
 +              }
 +
                /* load posts. */
                Set<Post> posts = new HashSet<Post>();
                while (true) {
                }
  
                logger.log(Level.INFO, "Saving Sone: %s", sone);
-               identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
                try {
+                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
                        /* save Sone into configuration. */
                        String sonePrefix = "Sone/" + sone.getId();
                        configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
                        configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
                        configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
  
 +                      /* save profile fields. */
 +                      int fieldCounter = 0;
 +                      for (Field profileField : profile.getFields()) {
 +                              String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
 +                              configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
 +                              configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
 +                      }
 +                      configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
 +
                        /* save posts. */
                        int postCounter = 0;
                        for (Post post : sone.getPosts()) {
                        logger.log(Level.INFO, "Sone %s saved.", sone);
                } catch (ConfigurationException ce1) {
                        logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
                }
        }
  
         */
        public void start() {
                loadConfiguration();
 +              updateChecker.addUpdateListener(this);
 +              updateChecker.start();
        }
  
        /**
                                soneInserter.stop();
                        }
                }
 +              updateChecker.stop();
 +              updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
                saveConfiguration();
                stopped = true;
  
                /* store the options first. */
                try {
 +                      configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").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/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
                        configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
                        configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
                        }
  
                }));
+               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
+               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-100));
+               options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
                }
  
                options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
+               options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
+               options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+               options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
                options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
  
                /* load known Sones. */
        public void ownIdentityAdded(OwnIdentity ownIdentity) {
                logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
                if (ownIdentity.hasContext("Sone")) {
+                       trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
                        addLocalSone(ownIdentity);
                }
        }
        @Override
        public void ownIdentityRemoved(OwnIdentity ownIdentity) {
                logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
+               trustedIdentities.remove(ownIdentity);
        }
  
        /**
         * {@inheritDoc}
         */
        @Override
-       public void identityAdded(Identity identity) {
+       public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
                logger.log(Level.FINEST, "Adding Identity: " + identity);
+               trustedIdentities.get(ownIdentity).add(identity);
                addRemoteSone(identity);
        }
  
         * {@inheritDoc}
         */
        @Override
-       public void identityUpdated(final Identity identity) {
+       public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
                new Thread(new Runnable() {
  
                        @Override
                        @SuppressWarnings("synthetic-access")
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId());
+                               sone.setIdentity(identity);
                                soneDownloader.fetchSone(sone);
                        }
                }).start();
         * {@inheritDoc}
         */
        @Override
-       public void identityRemoved(Identity identity) {
-               /* TODO */
+       public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
+               trustedIdentities.get(ownIdentity).remove(identity);
        }
  
 +      //
 +      // INTERFACE UpdateListener
 +      //
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void updateFound(Version version, long releaseTime) {
 +              coreListenerManager.fireUpdateFound(version, releaseTime);
 +      }
 +
  }
@@@ -25,8 -25,8 +25,8 @@@ import java.util.logging.Logger
  import net.pterodactylus.sone.core.Core;
  import net.pterodactylus.sone.core.FreenetInterface;
  import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
+ import net.pterodactylus.sone.freenet.plugin.PluginConnector;
  import net.pterodactylus.sone.freenet.wot.IdentityManager;
- import net.pterodactylus.sone.freenet.wot.PluginConnector;
  import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector;
  import net.pterodactylus.sone.web.WebInterface;
  import net.pterodactylus.util.config.Configuration;
@@@ -78,7 -78,7 +78,7 @@@ public class SonePlugin implements Fred
        }
  
        /** The version. */
 -      public static final Version VERSION = new Version(0, 3, 6, 4);
 +      public static final Version VERSION = new Version(0, 3, 7);
  
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
                        }
                }
  
 -              /* create freenet interface. */
 -              FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
 +              boolean startupFailed = true;
 +              try {
 +                      /* create freenet interface. */
 +                      FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
  
 -              /* create web of trust connector. */
 -              PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
 -              WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
 -              identityManager = new IdentityManager(webOfTrustConnector);
 -              identityManager.setContext("Sone");
 +                      /* create web of trust connector. */
 +                      PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
 +                      WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
 +                      identityManager = new IdentityManager(webOfTrustConnector);
 +                      identityManager.setContext("Sone");
  
 -              /* create core. */
 -              core = new Core(oldConfiguration, freenetInterface, identityManager);
 +                      /* create core. */
 +                      core = new Core(oldConfiguration, freenetInterface, identityManager);
  
 -              /* create the web interface. */
 -              webInterface = new WebInterface(this);
 -              core.addCoreListener(webInterface);
 +                      /* create the web interface. */
 +                      webInterface = new WebInterface(this);
 +                      core.addCoreListener(webInterface);
  
 -              /* create the identity manager. */
 -              identityManager.addIdentityListener(core);
 +                      /* create the identity manager. */
 +                      identityManager.addIdentityListener(core);
  
 -              /* start core! */
 -              boolean startupFailed = true;
 -              try {
 +                      /* start core! */
                        core.start();
                        if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) {
                                logger.log(Level.INFO, "Setting configuration to file-based configuration.");
@@@ -30,7 -30,6 +30,7 @@@ import net.pterodactylus.sone.data.Sone
  import net.pterodactylus.sone.freenet.wot.OwnIdentity;
  import net.pterodactylus.sone.web.page.Page.Request.Method;
  import net.pterodactylus.util.logging.Logging;
 +import net.pterodactylus.util.template.DataProvider;
  import net.pterodactylus.util.template.Template;
  import freenet.clients.http.ToadletContext;
  
@@@ -94,10 -93,10 +94,10 @@@ public class CreateSonePage extends Son
         * {@inheritDoc}
         */
        @Override
 -      protected void processTemplate(Request request, Template template) throws RedirectException {
 -              super.processTemplate(request, template);
 +      protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
 +              super.processTemplate(request, dataProvider);
                List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
 -              template.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
 +              dataProvider.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("identity", 44);
                        OwnIdentity selectedIdentity = null;
                                }
                        }
                        if (selectedIdentity == null) {
 -                              template.set("errorNoIdentity", true);
 +                              dataProvider.set("errorNoIdentity", true);
                                return;
                        }
                        /* create Sone. */
-                       webInterface.getCore().getIdentityManager().addContext(selectedIdentity, "Sone");
                        Sone sone = webInterface.getCore().createSone(selectedIdentity);
                        if (sone == null) {
                                logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: %s", selectedIdentity);
index 0000000,021fa6b..9cbbb55
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,68 +1,69 @@@
 -      protected void processTemplate(Request request, Template template) throws RedirectException {
 -              super.processTemplate(request, template);
+ /*
+  * Sone - TrustPage.java - Copyright © 2011 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
+  * the Free Software Foundation, either version 3 of the License, or
+  * (at your option) any later version.
+  *
+  * This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
+  */
+ package net.pterodactylus.sone.web;
+ import net.pterodactylus.sone.core.Core;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.web.page.Page.Request.Method;
++import net.pterodactylus.util.template.DataProvider;
+ import net.pterodactylus.util.template.Template;
+ /**
+  * Page that lets the user distrust another Sone. This will assign a
+  * configurable (negative) amount of trust to an identity.
+  *
+  * @see Core#distrustSone(Sone, Sone)
+  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+  */
+ public class DistrustPage extends SoneTemplatePage {
+       /**
+        * Creates a new “distrust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DistrustPage(Template template, WebInterface webInterface) {
+               super("distrust.html", template, "Page.Distrust.Title", webInterface, true);
+       }
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+       /**
+        * {@inheritDoc}
+        */
+       @Override
++      protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
++              super.processTemplate(request, dataProvider);
+               if (request.getMethod() == Method.POST) {
+                       String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().distrustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPath);
+               }
+       }
+ }
@@@ -20,7 -20,6 +20,7 @@@ package net.pterodactylus.sone.web
  import net.pterodactylus.sone.core.Options;
  import net.pterodactylus.sone.web.page.Page.Request.Method;
  import net.pterodactylus.util.number.Numbers;
 +import net.pterodactylus.util.template.DataProvider;
  import net.pterodactylus.util.template.Template;
  
  /**
@@@ -50,12 -49,21 +50,21 @@@ public class OptionsPage extends SoneTe
         * {@inheritDoc}
         */
        @Override
 -      protected void processTemplate(Request request, Template template) throws RedirectException {
 -              super.processTemplate(request, template);
 +      protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
 +              super.processTemplate(request, dataProvider);
                Options options = webInterface.getCore().getOptions();
                if (request.getMethod() == Method.POST) {
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
                        options.getIntegerOption("InsertionDelay").set(insertionDelay);
+                       Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3), options.getIntegerOption("PositiveTrust").getReal());
+                       options.getIntegerOption("PositiveTrust").set(positiveTrust);
+                       Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 3), options.getIntegerOption("NegativeTrust").getReal());
+                       options.getIntegerOption("NegativeTrust").set(negativeTrust);
+                       String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
+                       if (trustComment.trim().length() == 0) {
+                               trustComment = null;
+                       }
+                       options.getStringOption("TrustComment").set(trustComment);
                        boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
                        options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
                        webInterface.getCore().saveConfiguration();
                        throw new RedirectException(getPath());
                }
 -              template.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
 -              template.set("positive-trust", options.getIntegerOption("PositiveTrust").get());
 -              template.set("negative-trust", options.getIntegerOption("NegativeTrust").get());
 -              template.set("trust-comment", options.getStringOption("TrustComment").get());
 -              template.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
 -              template.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
 -              template.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
 +              dataProvider.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
++              dataProvider.set("positive-trust", options.getIntegerOption("PositiveTrust").get());
++              dataProvider.set("negative-trust", options.getIntegerOption("NegativeTrust").get());
++              dataProvider.set("trust-comment", options.getStringOption("TrustComment").get());
 +              dataProvider.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
 +              dataProvider.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
 +              dataProvider.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
        }
  
  }
index 0000000,ef64d62..85341ad
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,68 +1,69 @@@
 -      protected void processTemplate(Request request, Template template) throws RedirectException {
 -              super.processTemplate(request, template);
+ /*
+  * Sone - TrustPage.java - Copyright © 2011 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
+  * the Free Software Foundation, either version 3 of the License, or
+  * (at your option) any later version.
+  *
+  * This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
+  */
+ package net.pterodactylus.sone.web;
+ import net.pterodactylus.sone.core.Core;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.web.page.Page.Request.Method;
++import net.pterodactylus.util.template.DataProvider;
+ import net.pterodactylus.util.template.Template;
+ /**
+  * Page that lets the user trust another Sone. This will assign a configurable
+  * amount of trust to an identity.
+  *
+  * @see Core#trustSone(Sone, Sone)
+  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+  */
+ public class TrustPage extends SoneTemplatePage {
+       /**
+        * Creates a new “trust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public TrustPage(Template template, WebInterface webInterface) {
+               super("trust.html", template, "Page.Trust.Title", webInterface, true);
+       }
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+       /**
+        * {@inheritDoc}
+        */
+       @Override
++      protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
++              super.processTemplate(request, dataProvider);
+               if (request.getMethod() == Method.POST) {
+                       String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().trustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPath);
+               }
+       }
+ }
index 0000000,8126215..cc9e26d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,68 +1,69 @@@
 -      protected void processTemplate(Request request, Template template) throws RedirectException {
 -              super.processTemplate(request, template);
+ /*
+  * Sone - TrustPage.java - Copyright © 2011 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
+  * the Free Software Foundation, either version 3 of the License, or
+  * (at your option) any later version.
+  *
+  * This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
+  */
+ package net.pterodactylus.sone.web;
+ import net.pterodactylus.sone.core.Core;
+ import net.pterodactylus.sone.data.Sone;
+ import net.pterodactylus.sone.web.page.Page.Request.Method;
++import net.pterodactylus.util.template.DataProvider;
+ import net.pterodactylus.util.template.Template;
+ /**
+  * Page that lets the user untrust another Sone. This will remove all trust
+  * assignments for an identity.
+  *
+  * @see Core#untrustSone(Sone, Sone)
+  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+  */
+ public class UntrustPage extends SoneTemplatePage {
+       /**
+        * Creates a new “untrust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UntrustPage(Template template, WebInterface webInterface) {
+               super("untrust.html", template, "Page.Untrust.Title", webInterface, true);
+       }
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+       /**
+        * {@inheritDoc}
+        */
+       @Override
++      protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
++              super.processTemplate(request, dataProvider);
+               if (request.getMethod() == Method.POST) {
+                       String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().untrustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPath);
+               }
+       }
+ }
@@@ -20,6 -20,7 +20,7 @@@ package net.pterodactylus.sone.web
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.Reader;
+ import java.io.StringReader;
  import java.io.UnsupportedEncodingException;
  import java.util.ArrayList;
  import java.util.Collection;
@@@ -40,26 -41,26 +41,29 @@@ import net.pterodactylus.sone.data.Repl
  import net.pterodactylus.sone.data.Sone;
  import net.pterodactylus.sone.freenet.L10nFilter;
  import net.pterodactylus.sone.freenet.wot.Identity;
+ import net.pterodactylus.sone.freenet.wot.Trust;
  import net.pterodactylus.sone.main.SonePlugin;
  import net.pterodactylus.sone.notify.ListNotification;
  import net.pterodactylus.sone.template.CollectionAccessor;
  import net.pterodactylus.sone.template.CssClassNameFilter;
  import net.pterodactylus.sone.template.GetPagePlugin;
  import net.pterodactylus.sone.template.IdentityAccessor;
 +import net.pterodactylus.sone.template.JavascriptFilter;
  import net.pterodactylus.sone.template.NotificationManagerAccessor;
  import net.pterodactylus.sone.template.PostAccessor;
  import net.pterodactylus.sone.template.ReplyAccessor;
  import net.pterodactylus.sone.template.RequestChangeFilter;
  import net.pterodactylus.sone.template.SoneAccessor;
  import net.pterodactylus.sone.template.SubstringFilter;
+ import net.pterodactylus.sone.template.TrustAccessor;
  import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
  import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
  import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
 +import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
  import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
  import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
+ import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
 +import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
  import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
  import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
  import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
@@@ -70,10 -71,11 +74,12 @@@ import net.pterodactylus.sone.web.ajax.
  import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
  import net.pterodactylus.sone.web.ajax.MarkPostAsKnownPage;
  import net.pterodactylus.sone.web.ajax.MarkReplyAsKnownPage;
 +import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
+ import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
  import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
  import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
  import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
+ import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
  import net.pterodactylus.sone.web.page.PageToadlet;
  import net.pterodactylus.sone.web.page.PageToadletFactory;
  import net.pterodactylus.sone.web.page.StaticPage;
@@@ -92,7 -94,6 +98,7 @@@ import net.pterodactylus.util.template.
  import net.pterodactylus.util.template.TemplateProvider;
  import net.pterodactylus.util.template.XmlFilter;
  import net.pterodactylus.util.thread.Ticker;
 +import net.pterodactylus.util.version.Version;
  import freenet.clients.http.SessionManager;
  import freenet.clients.http.SessionManager.Session;
  import freenet.clients.http.ToadletContainer;
@@@ -146,9 -147,6 +152,9 @@@ public class WebInterface implements Co
        /** The “Sone locked” notification. */
        private final ListNotification<Sone> lockedSonesNotification;
  
 +      /** The “new version” notification. */
 +      private final TemplateNotification newVersionNotification;
 +
        /**
         * Creates a new web interface.
         *
                templateFactory.addAccessor(Reply.class, new ReplyAccessor(getCore(), templateFactory));
                templateFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
                templateFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
+               templateFactory.addAccessor(Trust.class, new TrustAccessor());
                templateFactory.addFilter("date", new DateFilter());
                templateFactory.addFilter("l10n", new L10nFilter(getL10n()));
                templateFactory.addFilter("substring", new SubstringFilter());
                templateFactory.addFilter("change", new RequestChangeFilter());
                templateFactory.addFilter("match", new MatchFilter());
                templateFactory.addFilter("css", new CssClassNameFilter());
 +              templateFactory.addFilter("js", new JavascriptFilter());
                templateFactory.addPlugin("getpage", new GetPagePlugin());
                templateFactory.addPlugin("paginate", new PaginationPlugin());
                templateFactory.setTemplateProvider(new ClassPathTemplateProvider(templateFactory));
  
                Template lockedSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
 +
 +              Template newVersionTemplate = templateFactory.createTemplate(createReader("/templates/notify/newVersionNotification.html"));
 +              newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
        }
  
        //
         * Register all toadlets.
         */
        private void registerToadlets() {
+               Template emptyTemplate = templateFactory.createTemplate(new StringReader(""));
                Template loginTemplate = templateFactory.createTemplate(createReader("/templates/login.html"));
                Template indexTemplate = templateFactory.createTemplate(createReader("/templates/index.html"));
                Template knownSonesTemplate = templateFactory.createTemplate(createReader("/templates/knownSones.html"));
                Template createPostTemplate = templateFactory.createTemplate(createReader("/templates/createPost.html"));
                Template createReplyTemplate = templateFactory.createTemplate(createReader("/templates/createReply.html"));
                Template editProfileTemplate = templateFactory.createTemplate(createReader("/templates/editProfile.html"));
 +              Template editProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/editProfileField.html"));
 +              Template deleteProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/deleteProfileField.html"));
                Template viewSoneTemplate = templateFactory.createTemplate(createReader("/templates/viewSone.html"));
                Template viewPostTemplate = templateFactory.createTemplate(createReader("/templates/viewPost.html"));
-               Template likePostTemplate = templateFactory.createTemplate(createReader("/templates/like.html"));
-               Template unlikePostTemplate = templateFactory.createTemplate(createReader("/templates/unlike.html"));
                Template deletePostTemplate = templateFactory.createTemplate(createReader("/templates/deletePost.html"));
                Template deleteReplyTemplate = templateFactory.createTemplate(createReader("/templates/deleteReply.html"));
-               Template lockSoneTemplate = templateFactory.createTemplate(createReader("/templates/lockSone.html"));
-               Template unlockSoneTemplate = templateFactory.createTemplate(createReader("/templates/unlockSone.html"));
-               Template followSoneTemplate = templateFactory.createTemplate(createReader("/templates/followSone.html"));
-               Template unfollowSoneTemplate = templateFactory.createTemplate(createReader("/templates/unfollowSone.html"));
                Template deleteSoneTemplate = templateFactory.createTemplate(createReader("/templates/deleteSone.html"));
                Template noPermissionTemplate = templateFactory.createTemplate(createReader("/templates/noPermission.html"));
-               Template dismissNotificationTemplate = templateFactory.createTemplate(createReader("/templates/dismissNotification.html"));
-               Template logoutTemplate = templateFactory.createTemplate(createReader("/templates/logout.html"));
                Template optionsTemplate = templateFactory.createTemplate(createReader("/templates/options.html"));
                Template aboutTemplate = templateFactory.createTemplate(createReader("/templates/about.html"));
 +              Template invalidTemplate = templateFactory.createTemplate(createReader("/templates/invalid.html"));
                Template postTemplate = templateFactory.createTemplate(createReader("/templates/include/viewPost.html"));
                Template replyTemplate = templateFactory.createTemplate(createReader("/templates/include/viewReply.html"));
  
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfilePage(editProfileTemplate, this), "EditProfile"));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldPage(editProfileFieldTemplate, this)));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostPage(createPostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyPage(createReplyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewSonePage(viewSoneTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewPostPage(viewPostTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new LikePage(likePostTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikePage(unlikePostTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new LikePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostPage(deletePostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyPage(deleteReplyTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new LockSonePage(lockSoneTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSonePage(unlockSoneTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(followSoneTemplate, this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(unfollowSoneTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new LockSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteSonePage(deleteSoneTemplate, this), "DeleteSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(logoutTemplate, this), "Logout"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(dismissNotificationTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
 +              pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
  
                ToadletContainer toadletContainer = sonePlugin.pluginRespirator().getToadletContainer();
                toadletContainer.getPageMaker().addNavigationCategory("/Sone/index.html", "Navigation.Menu.Name", "Navigation.Menu.Tooltip", sonePlugin);
        }
  
        /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void updateFound(Version version, long releaseTime) {
 +              newVersionNotification.set("version", version);
 +              newVersionNotification.set("releaseTime", releaseTime);
 +              notificationManager.addNotification(newVersionNotification);
 +      }
 +
 +      /**
         * Template provider implementation that uses
         * {@link WebInterface#createReader(String)} to load templates for
         * inclusion.
@@@ -27,6 -27,10 +27,10 @@@ Page.Options.Page.Title=Option
  Page.Options.Page.Description=These options influence the runtime behaviour of the Sone plugin.
  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.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.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.Section.Cleaning.Title=Clean Up
@@@ -70,32 -74,8 +74,32 @@@ Page.EditProfile.Birthday.Title=Birthda
  Page.EditProfile.Birthday.Label.Day=Day:
  Page.EditProfile.Birthday.Label.Month=Month:
  Page.EditProfile.Birthday.Label.Year=Year:
 -Page.EditProfile.Page.Status.Changed=Your changes have been saved and will be inserted shortly.
 +Page.EditProfile.Fields.Title=Custom Fields
 +Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more.
 +Page.EditProfile.Fields.Button.Edit=edit
 +Page.EditProfile.Fields.Button.MoveUp=move up
 +Page.EditProfile.Fields.Button.MoveDown=move down
 +Page.EditProfile.Fields.Button.Delete=delete
 +Page.EditProfile.Fields.Button.ReallyDelete=really delete
 +Page.EditProfile.Fields.AddField.Title=Add Field
 +Page.EditProfile.Fields.AddField.Label.Name=Name:
 +Page.EditProfile.Fields.AddField.Button.AddField=Add Field
  Page.EditProfile.Button.Save=Save Profile
 +Page.EditProfile.Error.DuplicateFieldName=The field name “{fieldName}” does already exist.
 +
 +Page.EditProfileField.Title=Edit Profile Field - Sone
 +Page.EditProfileField.Page.Title=Edit Profile Field
 +Page.EditProfileField.Text=Enter a new name for this profile field.
 +Page.EditProfileField.Error.DuplicateFieldName=The field name you entered does already exist.
 +Page.EditProfileField.Button.Save=Change
 +Page.EditProfileField.Button.Reset=Revert to old name
 +Page.EditProfileField.Button.Cancel=Do not change name
 +
 +Page.DeleteProfileField.Title=Delete Profile Field - Sone
 +Page.DeleteProfileField.Page.Title=Delete Profile Field
 +Page.DeleteProfileField.Text=Do you really want to delete this profile field?
 +Page.DeleteProfileField.Button.Yes=Yes, delete
 +Page.DeleteProfileField.Button.No=No, do not delete
  
  Page.CreatePost.Title=Create Post - Sone
  Page.CreatePost.Page.Title=Create Post
@@@ -116,8 -96,6 +120,8 @@@ Page.ViewSone.UnknownSone.Description=T
  Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
  Page.ViewSone.PostList.Title=Posts by {sone}
  Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
 +Page.ViewSone.Profile.Title=Profile
 +Page.ViewSone.Profile.Label.Name=Name
  
  Page.ViewPost.Title=View Post - Sone
  Page.ViewPost.Page.Title=View Post by {sone}
@@@ -147,6 -125,12 +151,12 @@@ Page.FollowSone.Title=Follow Sone - Son
  
  Page.UnfollowSone.Title=Unfollow Sone - Sone
  
+ Page.Trust.Title=Trust Sone - Sone
+ Page.Distrust.Title=Distrust Sone - Sone
+ Page.Untrust.Title=Untrust Sone - Sone
  Page.NoPermission.Title=Unauthorized Access - Sone
  Page.NoPermission.Page.Title=Unauthorized Access
  Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
@@@ -158,10 -142,6 +168,10 @@@ Page.WotPluginMissing.Text.LoadPlugin=P
  
  Page.Logout.Title=Logout - Sone
  
 +Page.Invalid.Title=Invalid Action Performed
 +Page.Invalid.Page.Title=Invalid Action Performed
 +Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
 +
  View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
  View.CreateSone.Select.Default=Select an identity
  View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
@@@ -189,6 -169,10 +199,10 @@@ View.Post.Reply.DeleteLink=Delet
  View.Post.LikeLink=Like
  View.Post.UnlikeLink=Unlike
  
+ View.Trust.Tooltip.Trust=Trust this person
+ View.Trust.Tooltip.Distrust=Assign negative trust to this person
+ View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
  WebInterface.DefaultText.StatusUpdate=What’s on your mind?
  WebInterface.DefaultText.Message=Write a Message…
  WebInterface.DefaultText.Reply=Write a Reply…
@@@ -198,7 -182,6 +212,7 @@@ WebInterface.DefaultText.LastName=Last 
  WebInterface.DefaultText.BirthDay=Day
  WebInterface.DefaultText.BirthMonth=Month
  WebInterface.DefaultText.BirthYear=Year
 +WebInterface.DefaultText.FieldName=Field name
  WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
  WebInterface.Confirmation.DeletePostButton=Yes, delete!
  WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
@@@ -206,8 -189,6 +220,8 @@@ WebInterface.SelectBox.Choose=Chooseâ\80
  WebInterface.SelectBox.Yes=Yes
  WebInterface.SelectBox.No=No
  WebInterface.ClickToShow.Replies=Click here to show hidden replies.
 +WebInterface.VersionInformation.CurrentVersion=Current Version:
 +WebInterface.VersionInformation.LatestVersion=Latest Version:
  
  Notification.ClickHereToRead=Click here to read the full text of the notification.
  Notification.FirstStart.Text=This seems to be the first time you start Sone. To start, create a new Sone from a web of trust identity and start following other Sones.
@@@ -224,4 -205,3 +238,4 @@@ Notification.SoneIsBeingRescued.Text=Th
  Notification.SoneRescued.Text=The following Sones have been rescued:
  Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
  Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
 +Notification.NewVersion.Text=A new version of the Sone plugin was found: Version {version}.
@@@ -1,7 -1,7 +1,7 @@@
  /* Sone Main CSS File */
  
  /* first, override some fproxy rules. */
- #sone .post .reply div,#sone .post .time,#sone .post .delete,#sone .post .show-reply-form, input[type=text], textarea {
+ #sone div, #sone span, #sone .post .time,#sone .post .delete,#sone .post .show-reply-form, input[type=text], textarea {
        font: inherit;
  }
  
@@@ -16,21 -16,10 +16,21 @@@ input[type=text], textarea 
        outline: none;
  }
  
 +input[type=text].short {
 +      width: 25em;
 +}
 +
  textarea {
        height: 4em;
  }
  
 +#sone button {
 +      background-color: #ddd;
 +      border-width: 1px;
 +      color: #444;
 +      padding: 0.5ex 1.5ex;
 +}
 +
  #sone form {
        margin: 0px;
  }
        color: rgb(255, 172, 0);
  }
  
 +#sone a.link {
 +      cursor: pointer;
 +}
 +
  #sone a.internet {
        color: rgb(255, 0, 0);
  }
        font-size: 85%;
  }
  
+ #sone .separator {
+       font: inherit;
+       color: rgb(28, 131, 191);
+ }
  #sone .post .time {
        display: inline;
        color: #666;
  }
  
- #sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike {
+ #sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike, #sone .post .trust, #sone .post .distrust, #sone .post .untrust {
        display: inline;
        font: inherit;
+       margin: 0px;
  }
  
  #sone .post .likes.hidden {
        display: none;
  }
  
- #sone .post .like.hidden, #sone .post .unlike.hidden {
+ #sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden {
        display: none;
  }
  
- #sone .post .delete button, #sone .post .like button, #sone .post .unlike button {
+ #sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button {
        border: 0px;
        background: none;
        padding: 0px;
        color: rgb(28, 131, 191);
        font: inherit;
+       margin: 0px;
+ }
+ #sone .post .trust button {
+       color: rgb(0, 128, 0);
  }
  
- #sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover {
+ #sone .post .distrust button {
+       color: rgb(255, 0, 0);
+ }
+ #sone .post .untrust button {
+       color: rgb(64, 64, 64);
+ }
+ #sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover {
        border: 0px;
        background: none;
        padding: 0px;
        cursor: pointer;
  }
  
- #sone .post .delete:before, #sone .post .likes:before, #sone .post .like:before, #sone .post .unlike:before {
-       content: ' ‧ ';
- }
  #sone .post .likes span {
        font: inherit;
        color: green;
        color: rgb(255, 172, 0);
  }
  
- #sone .post .show-reply-form:before {
-       content: ' ‧ ';
- }
  #sone .post .create-reply {
        clear: both;
        background-color: #f0f0ff;
        display: inline;
  }
  
 -#sone #create-sone {
 +#sone .profile-field, #sone #edit-profile button[type=submit], #sone #delete-profile-field {
 +      margin-top: 1em;
 +}
  
 +#sone .profile-field .name {
 +      display: inline;
 +      font-weight: bold;
 +}
 +
 +#sone .profile-field .name.hidden {
 +      display: none;
 +}
 +
 +#sone .profile-field button.confirm {
 +      font-weight: bold;
 +      color: #080;
 +}
 +
 +#sone .profile-field button.cancel {
 +      font-weight: bold;
 +      color: red;
 +}
 +
 +#sone .profile-field .value {
 +      margin-left: 2em;
 +}
 +
 +#sone #edit-profile .profile-field .value {
 +      margin-left: inherit;
 +}
 +
 +#sone .profile-field .edit-field-name, #sone .profile-field .move-up-field, #sone .profile-field .move-down-field, #sone .profile-field .delete-field-name {
 +      float: right;
 +      margin-top: -1ex;
  }
  
  #sone #tail {
        color: #888;
  }
  
 +#sone #tail #version-information {
 +      margin-top: 1em;
 +}
 +
  #sone #add-sone textarea, #sone #create-sone textarea, #sone #load-sone textarea, #sone #edit-profile textarea {
        height: 1.5em;
  }
  }
  
  #sone .confirm {
 -      font-weight: bold !important;
 -      color: red !important;
 +      font-weight: bold;
 +      color: red;
  }
@@@ -68,6 -68,7 +68,7 @@@ function addCommentLink(postId, element
                return;
        }
        commentElement = (function(postId) {
+               separator = $("<span> · </span>").addClass("separator");
                var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
                        markPostAsKnown(getPostElement(this));
                        replyElement = $("#sone .post#" + postId + " .create-reply");
@@@ -87,6 -88,7 +88,7 @@@
                return commentElement;
        })(postId);
        $(insertAfterThisElement).after(commentElement.clone(true));
+       $(insertAfterThisElement).after(separator);
  }
  
  var translations = {};
@@@ -301,6 -303,17 +303,17 @@@ function getPostTime(element) 
        return getPostElement(element).find(".post-time").text();
  }
  
+ /**
+  * Returns the author of the post the given element belongs to.
+  *
+  * @param element
+  *            The element whose post to get the author for
+  * @returns The ID of the authoring Sone
+  */
+ function getPostAuthor(element) {
+       return getPostElement(element).find(".post-author").text();
+ }
  function getReplyElement(element) {
        return $(element).closest(".reply");
  }
@@@ -313,6 -326,17 +326,17 @@@ function getReplyTime(element) 
        return getReplyElement(element).find(".reply-time").text();
  }
  
+ /**
+  * Returns the author of the reply the given element belongs to.
+  *
+  * @param element
+  *            The element whose reply to get the author for
+  * @returns The ID of the authoring Sone
+  */
+ function getReplyAuthor(element) {
+       return getReplyElement(element).find(".reply-author").text();
+ }
  function likePost(postId) {
        $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
@@@ -377,6 -401,74 +401,74 @@@ function unlikeReply(replyId) 
        });
  }
  
+ /**
+  * Trusts the Sone with the given ID.
+  *
+  * @param soneId
+  *            The ID of the Sone to trust
+  */
+ function trustSone(soneId) {
+       $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+ }
+ /**
+  * Distrusts the Sone with the given ID, i.e. assigns a negative trust value.
+  *
+  * @param soneId
+  *            The ID of the Sone to distrust
+  */
+ function distrustSone(soneId) {
+       $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+ }
+ /**
+  * Untrusts the Sone with the given ID, i.e. removes any trust assignment.
+  *
+  * @param soneId
+  *            The ID of the Sone to untrust
+  */
+ function untrustSone(soneId) {
+       $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+ }
+ /**
+  * Updates the trust controls for all posts and replies of the given Sone,
+  * according to the given trust value.
+  *
+  * @param soneId
+  *            The ID of the Sone to update all trust controls for
+  * @param trustValue
+  *            The trust value for the Sone
+  */
+ function updateTrustControls(soneId, trustValue) {
+       $("#sone .post").each(function() {
+               if (getPostAuthor(this) == soneId) {
+                       getPostElement(this).find(".post-trust").toggleClass("hidden", trustValue != null);
+                       getPostElement(this).find(".post-distrust").toggleClass("hidden", (trustValue != null) && (trustValue < 0));
+                       getPostElement(this).find(".post-untrust").toggleClass("hidden", trustValue == null);
+               }
+       });
+       $("#sone .reply").each(function() {
+               if (getReplyAuthor(this) == soneId) {
+                       getReplyElement(this).find(".reply-trust").toggleClass("hidden", trustValue != null);
+                       getReplyElement(this).find(".reply-distrust").toggleClass("hidden", (trustValue != null) && (trustValue < 0));
+                       getReplyElement(this).find(".reply-untrust").toggleClass("hidden", trustValue == null);
+               }
+       });
+ }
  function updateReplyLikes(replyId) {
        $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
@@@ -453,7 -545,7 +545,7 @@@ function ajaxifyPost(postElement) 
                        postReply(postId, text, function(success, error, replyId) {
                                if (success) {
                                        $(inputField).val("");
 -                                      loadNewReply(replyId);
 +                                      loadNewReply(replyId, getCurrentSoneId(), postId);
                                        markPostAsKnown(getPostElement(inputField));
                                        $("#sone .post#" + postId + " .create-reply").addClass("hidden");
                                } else {
                return false;
        });
  
+       /* convert trust control buttons to javascript functions. */
+       $(postElement).find(".post-trust").submit(function() {
+               trustSone(getPostAuthor(this));
+               return false;
+       });
+       $(postElement).find(".post-distrust").submit(function() {
+               distrustSone(getPostAuthor(this));
+               return false;
+       });
+       $(postElement).find(".post-untrust").submit(function() {
+               untrustSone(getPostAuthor(this));
+               return false;
+       });
        /* add “comment” link. */
        addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
  
        });
  
        /* mark everything as known on click. */
 -      $(postElement).click(function() {
 +      $(postElement).click(function(event) {
 +              if ($(event.target).hasClass("click-to-show")) {
 +                      return false;
 +              }
                markPostAsKnown(this);
        });
  
@@@ -537,6 -640,20 +643,20 @@@ function ajaxifyReply(replyElement) 
        })(replyElement);
        addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
  
+       /* convert trust control buttons to javascript functions. */
+       $(replyElement).find(".reply-trust").submit(function() {
+               trustSone(getReplyAuthor(this));
+               return false;
+       });
+       $(replyElement).find(".reply-distrust").submit(function() {
+               distrustSone(getReplyAuthor(this));
+               return false;
+       });
+       $(replyElement).find(".reply-untrust").submit(function() {
+               untrustSone(getReplyAuthor(this));
+               return false;
+       });
        /* mark post and all replies as known on click. */
        $(replyElement).click(function() {
                markPostAsKnown(getPostElement(this));
@@@ -588,11 -705,11 +708,11 @@@ function getStatus() 
                        });
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
 -                              loadNewPost(value);
 +                              loadNewPost(value.id, value.sone, value.recipient, value.time);
                        });
                        /* process new replies. */
                        $.each(data.newReplies, function(index, value) {
 -                              loadNewReply(value);
 +                              loadNewReply(value.id, value.sone, value.post, value.postSone);
                        });
                        /* do it again in 5 seconds. */
                        setTimeout(getStatus, 5000);
  }
  
  /**
 + * Returns the ID of the currently logged in Sone.
 + *
 + * @return The ID of the current Sone, or an empty string if no Sone is logged
 + *         in
 + */
 +function getCurrentSoneId() {
 +      return $("#currentSoneId").text();
 +}
 +
 +/**
   * Returns the content of the page-id attribute.
   *
   * @returns The page ID
@@@ -709,20 -816,10 +829,20 @@@ function hasReply(replyId) 
        return $("#sone .reply#" + replyId).length > 0;
  }
  
 -function loadNewPost(postId) {
 +function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
 +      if (!isIndexPage()) {
 +              if (!isViewPostPage() || (getShownPostId() != postId)) {
 +                      if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
 +                              return;
 +                      }
 +              }
 +      }
 +      if (getPostTime($("#sone .post").last()) > time) {
 +              return;
 +      }
        $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
                        newPost = $(data.post.html).addClass("hidden");
                        if (firstOlderPost != null) {
                                newPost.insertBefore(firstOlderPost);
 -                      } else {
 -                              $("#sone #posts").append(newPost);
                        }
                        ajaxifyPost(newPost);
                        newPost.slideDown();
        });
  }
  
 -function loadNewReply(replyId) {
 +function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (hasReply(replyId)) {
                return;
        }
 +      if (!hasPost(postId)) {
 +              return;
 +      }
        $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
@@@ -796,7 -892,6 +916,7 @@@ function markPostAsKnown(postElements) 
                        (function(postElement) {
                                $.getJSON("markPostAsKnown.ajax", {"formPassword": getFormPassword(), "post": getPostId(postElement)}, function(data, textStatus) {
                                        $(postElement).removeClass("new");
 +                                      $(".click-to-show", postElement).removeClass("new");
                                });
                        })(postElement);
                }
@@@ -866,80 -961,6 +986,80 @@@ function showNotificationDetails(notifi
        $("#sone .notification#" + notificationId + " .short-text").hide();
  }
  
 +/**
 + * Deletes the field with the given ID from the profile.
 + *
 + * @param fieldId
 + *            The ID of the field to delete
 + */
 +function deleteProfileField(fieldId) {
 +      $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
 +              if (data && data.success) {
 +                      $("#sone .profile-field#" + data.field.id).slideUp();
 +              }
 +      });
 +}
 +
 +/**
 + * Renames a profile field.
 + *
 + * @param fieldId
 + *            The ID of the field to rename
 + * @param newName
 + *            The new name of the field
 + * @param successFunction
 + *            Called when the renaming was successful
 + */
 +function editProfileField(fieldId, newName, successFunction) {
 +      $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
 +              if (data && data.success) {
 +                      successFunction();
 +              }
 +      });
 +}
 +
 +/**
 + * Moves the profile field with the given ID one slot in the given direction.
 + *
 + * @param fieldId
 + *            The ID of the field to move
 + * @param direction
 + *            The direction to move in (“up” or “down”)
 + * @param successFunction
 + *            Function to call on success
 + */
 +function moveProfileField(fieldId, direction, successFunction) {
 +      $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
 +              if (data && data.success) {
 +                      successFunction();
 +              }
 +      });
 +}
 +
 +/**
 + * Moves the profile field with the given ID up one slot.
 + *
 + * @param fieldId
 + *            The ID of the field to move
 + * @param successFunction
 + *            Function to call on success
 + */
 +function moveProfileFieldUp(fieldId, successFunction) {
 +      moveProfileField(fieldId, "up", successFunction);
 +}
 +
 +/**
 + * Moves the profile field with the given ID down one slot.
 + *
 + * @param fieldId
 + *            The ID of the field to move
 + * @param successFunction
 + *            Function to call on success
 + */
 +function moveProfileFieldDown(fieldId, successFunction) {
 +      moveProfileField(fieldId, "down", successFunction);
 +}
 +
  //
  // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
  //
@@@ -958,7 -979,7 +1078,7 @@@ $(document).ready(function() 
                        text = $(this).find(":input:enabled").val();
                        $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
 -                                      loadNewPost(data.postId);
 +                                      loadNewPost(data.postId, getCurrentSoneId());
                                }
                        });
                        $(this).find(":input:enabled").val("").blur();
                        text = $(this).find(":input:enabled").val();
                        $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
 -                                      loadNewPost(data.postId);
 +                                      loadNewPost(data.postId, getCurrentSoneId());
                                }
                        });
                        $(this).find(":input:enabled").val("").blur();