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.

80 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/Options.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneException.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
src/main/java/net/pterodactylus/sone/data/Post.java
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java
src/main/java/net/pterodactylus/sone/freenet/StringBucket.java
src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListener.java
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotification.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java [deleted file]
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java
src/main/java/net/pterodactylus/sone/text/ParserContext.java
src/main/java/net/pterodactylus/sone/text/TextFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/LikePage.java
src/main/java/net/pterodactylus/sone/web/LoginPage.java
src/main/java/net/pterodactylus/sone/web/LogoutPage.java
src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
src/main/java/net/pterodactylus/sone/web/UntrustPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/Page.java
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/java/net/pterodactylus/sone/web/page/PageToadletFactory.java
src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/StaticPage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/sone-offline.png [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/include/head.html
src/main/resources/templates/include/pagination.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/include/viewSone.html
src/main/resources/templates/index.html
src/main/resources/templates/insert/index.html
src/main/resources/templates/notify/newPostNotification.html
src/main/resources/templates/notify/newReplyNotification.html
src/main/resources/templates/notify/newSoneNotification.html
src/main/resources/templates/options.html
src/main/resources/templates/viewSone.html
src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java

diff --git a/pom.xml b/pom.xml
index 21f47ee..f88c9c7 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.6</version>
+       <version>0.6.4</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.9.3-SNAPSHOT</version>
+                       <version>0.9.6-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index cfe53ea..765b672 100644 (file)
@@ -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;
 
@@ -50,6 +53,7 @@ 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.IntegerRangeValidator;
 import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
 import freenet.keys.FreenetURI;
@@ -108,6 +112,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** 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;
 
@@ -891,6 +898,9 @@ public class Core implements IdentityListener, UpdateListener {
                        return null;
                }
                Sone sone = addLocalSone(ownIdentity);
+               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+               sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
+               saveSone(sone);
                return sone;
        }
 
@@ -930,15 +940,15 @@ public class Core implements IdentityListener, UpdateListener {
                        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;
                }
        }
@@ -1184,6 +1194,9 @@ public class Core implements IdentityListener, UpdateListener {
                        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);
@@ -1284,7 +1297,6 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /* 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. */
@@ -1508,6 +1520,7 @@ public class Core implements IdentityListener, UpdateListener {
                synchronized (posts) {
                        posts.remove(post.getId());
                }
+               coreListenerManager.firePostRemoved(post);
                synchronized (newPosts) {
                        markPostKnown(post);
                        knownPosts.remove(post.getId());
@@ -1704,6 +1717,7 @@ public class Core implements IdentityListener, UpdateListener {
                        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());
@@ -1771,7 +1785,7 @@ public class Core implements IdentityListener, UpdateListener {
        @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) {
@@ -1779,9 +1793,10 @@ public class Core implements IdentityListener, UpdateListener {
                        }
 
                }));
-               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>() {
 
@@ -1815,10 +1830,11 @@ public class Core implements IdentityListener, UpdateListener {
                        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));
@@ -1875,6 +1891,21 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * 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
@@ -1938,6 +1969,7 @@ public class Core implements IdentityListener, UpdateListener {
                        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);
                        }
@@ -1950,6 +1982,48 @@ public class Core implements IdentityListener, UpdateListener {
        @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());
+               }
        }
 
        //
@@ -1995,6 +2069,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2017,6 +2103,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2029,6 +2127,27 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2038,6 +2157,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2060,6 +2191,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
index e22f407..20c8da7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - FreenetInterface.java - Copyright © 2010 David Roden
+ * Sone - FreenetInterface.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
index b7ece81..7392da2 100644 (file)
@@ -1,3 +1,20 @@
+/*
+ * Sone - Options.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
+ * 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.core;
 
 import java.util.ArrayList;
@@ -7,6 +24,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import net.pterodactylus.util.validation.Validator;
+
 /**
  * Stores various options that influence Sone’s behaviour.
  *
@@ -47,12 +66,26 @@ public class Options {
                public T getReal();
 
                /**
+                * Validates the given value. Note that {@code null} is always a valid
+                * value!
+                *
+                * @param value
+                *            The value to validate
+                * @return {@code true} if this option does not have a {@link Validator}
+                *         , or the {@link Validator} validates this object, {@code
+                *         false} otherwise
+                */
+               public boolean validate(T value);
+
+               /**
                 * Sets the current value of the option.
                 *
                 * @param value
                 *            The new value of the option
+                * @throws IllegalArgumentException
+                *             if the value is not valid for this option
                 */
-               public void set(T value);
+               public void set(T value) throws IllegalArgumentException;
 
        }
 
@@ -96,6 +129,9 @@ public class Options {
                /** The current value. */
                private volatile T value;
 
+               /** The validator. */
+               private Validator<T> validator;
+
                /** The option watcher. */
                private final List<OptionWatcher<T>> optionWatchers = new ArrayList<OptionWatcher<T>>();
 
@@ -108,7 +144,22 @@ public class Options {
                 *            The option watchers
                 */
                public DefaultOption(T defaultValue, OptionWatcher<T>... optionWatchers) {
+                       this(defaultValue, null, optionWatchers);
+               }
+
+               /**
+                * Creates a new default option.
+                *
+                * @param defaultValue
+                *            The default value of the option
+                * @param validator
+                *            The validator for value validation
+                * @param optionWatchers
+                *            The option watchers
+                */
+               public DefaultOption(T defaultValue, Validator<T> validator, OptionWatcher<T>... optionWatchers) {
                        this.defaultValue = defaultValue;
+                       this.validator = validator;
                        this.optionWatchers.addAll(Arrays.asList(optionWatchers));
                }
 
@@ -142,8 +193,18 @@ public class Options {
                /**
                 * {@inheritDoc}
                 */
+               public boolean validate(T value) {
+                       return (validator == null) || (value == null) || validator.validate(value);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
                @Override
                public void set(T value) {
+                       if ((value != null) && (validator != null) && (!validator.validate(value))) {
+                               throw new IllegalArgumentException("New Value (" + value + ") could not be validated.");
+                       }
                        T oldValue = this.value;
                        this.value = value;
                        if (!get().equals(oldValue)) {
index 7022f5e..56c1e4a 100644 (file)
@@ -119,7 +119,7 @@ public class SoneDownloader extends AbstractService {
         *            The Sone to fetch
         */
        public void fetchSone(Sone sone) {
-               fetchSone(sone, sone.getRequestUri());
+               fetchSone(sone, sone.getRequestUri().sskForUSK());
        }
 
        /**
index 74f257d..c786661 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - SoneException.java - Copyright © 2010 David Roden
+ * Sone - SoneException.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
index 15e054f..0abebc3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - SoneInserter.java - Copyright © 2010 David Roden
+ * Sone - SoneInserter.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
@@ -229,7 +229,6 @@ public class SoneInserter extends AbstractService {
                                        synchronized (sone) {
                                                if (lastInsertFingerprint.equals(sone.getFingerprint())) {
                                                        logger.log(Level.FINE, "Sone “%s” was not modified further, resetting counter…", new Object[] { sone });
-                                                       core.saveSone(sone);
                                                        lastModificationTime = 0;
                                                        modified = false;
                                                }
@@ -248,7 +247,7 @@ public class SoneInserter extends AbstractService {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private static class InsertInformation {
+       private class InsertInformation {
 
                /** All properties of the Sone, copied for thread safety. */
                private final Map<String, Object> soneProperties = new HashMap<String, Object>();
@@ -347,6 +346,7 @@ public class SoneInserter extends AbstractService {
 
                        TemplateContext templateContext = templateContextFactory.createTemplateContext();
                        templateContext.set("currentSone", soneProperties);
+                       templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
                        templateContext.set("version", SonePlugin.VERSION);
                        StringWriter writer = new StringWriter();
                        StringBucket bucket = null;
index e07e160..9c50f37 100644 (file)
@@ -49,7 +49,7 @@ public class UpdateChecker {
        private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
 
        /** The current latest known edition. */
-       private static final int LATEST_EDITION = 25;
+       private static final int LATEST_EDITION = 36;
 
        /** The Freenet interface. */
        private final FreenetInterface freenetInterface;
index bb431d0..a28bf9f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - StatusUpdate.java - Copyright © 2010 David Roden
+ * Sone - Post.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
index 8d4306d..6b44d10 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - Profile.java - Copyright © 2010 David Roden
+ * Sone - Profile.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
index 2dd4bc1..715d95b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - Sone.java - Copyright © 2010 David Roden
+ * Sone - Sone.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
index ef1a183..fa7385c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - L10nFilter.java - Copyright © 2010 David Roden
+ * Sone - L10nFilter.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
index 5e30de1..5aa68cf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - PluginStoreConfigurationBackend.java - Copyright © 2010 David Roden
+ * Sone - PluginStoreConfigurationBackend.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
index ebe036a..2993b2f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - TemplateBucket.java - Copyright © 2010 David Roden
+ * Sone - StringBucket.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
index 71710e4..3da08f3 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.freenet.plugin;
 
 import java.util.EventListener;
 
-
 import freenet.support.SimpleFieldSet;
 import freenet.support.api.Bucket;
 
index 3c04823..46f81d6 100644 (file)
@@ -64,6 +64,7 @@ public class DefaultIdentity implements Identity {
        private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
 
        /** Cached trust. */
+       /* synchronize on itself. */
        private final WritableCache<OwnIdentity, Trust> trustCache = new MemoryCache<OwnIdentity, Trust>(new ValueRetriever<OwnIdentity, Trust>() {
 
                @Override
@@ -249,7 +250,9 @@ public class DefaultIdentity implements Identity {
        @Override
        public Trust getTrust(OwnIdentity ownIdentity) {
                try {
-                       return trustCache.get(ownIdentity);
+                       synchronized (trustCache) {
+                               return trustCache.get(ownIdentity);
+                       }
                } catch (CacheException ce1) {
                        logger.log(Level.WARNING, "Could not get trust for OwnIdentity: " + ownIdentity, ce1);
                        return null;
@@ -265,7 +268,9 @@ public class DefaultIdentity implements Identity {
         *            The trust received for this identity
         */
        void setTrustPrivate(OwnIdentity ownIdentity, Trust trust) {
-               trustCache.put(ownIdentity, trust);
+               synchronized (trustCache) {
+                       trustCache.put(ownIdentity, trust);
+               }
        }
 
        //
index 98ba2c7..3c97e55 100644 (file)
@@ -180,21 +180,18 @@ public class IdentityManager extends AbstractService {
                        Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
                        Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
 
+                       Set<OwnIdentity> ownIdentities = null;
+                       boolean identitiesLoaded = false;
                        try {
                                /* get all identities with the wanted context from WoT. */
-                               Set<OwnIdentity> ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
+                               ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
 
-                               /* check for changes. */
-                               for (OwnIdentity ownIdentity : ownIdentities) {
-                                       currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
-                               }
-                               checkOwnIdentities(currentOwnIdentities);
-
-                               /* now filter for context and get all identities. */
+                               /* load trusted identities. */
                                for (OwnIdentity ownIdentity : ownIdentities) {
                                        if ((context != null) && !ownIdentity.hasContext(context)) {
                                                continue;
                                        }
+                                       currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
 
                                        Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context);
                                        Map<String, Identity> identities = new HashMap<String, Identity>();
@@ -202,6 +199,19 @@ public class IdentityManager extends AbstractService {
                                        for (Identity identity : trustedIdentities) {
                                                identities.put(identity.getId(), identity);
                                        }
+                               }
+                               identitiesLoaded = true;
+                       } catch (WebOfTrustException wote1) {
+                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+                       }
+
+                       if (identitiesLoaded) {
+
+                               /* check for changes. */
+                               checkOwnIdentities(currentOwnIdentities);
+
+                               /* now check for changes in remote identities. */
+                               for (OwnIdentity ownIdentity : currentOwnIdentities.values()) {
 
                                        /* find new identities. */
                                        for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) {
@@ -258,13 +268,10 @@ public class IdentityManager extends AbstractService {
                                                        }
                                                }
                                        }
-
-                                       /* remember the current set of identities. */
-                                       oldIdentities = currentIdentities;
                                }
 
-                       } catch (WebOfTrustException wote1) {
-                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+                               /* remember the current set of identities. */
+                               oldIdentities = currentIdentities;
                        }
 
                        /* wait a minute before checking again. */
index 47e6d33..6259fd3 100644 (file)
@@ -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
@@ -83,7 +83,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        /** 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);
index c66d376..3a79c93 100644 (file)
@@ -101,7 +101,8 @@ public class ListNotification<T> extends TemplateNotification {
        }
 
        /**
-        * Sets the elements to show in this notification.
+        * Sets the elements to show in this notification. This method will not call
+        * {@link #touch()}.
         *
         * @param elements
         *            The elements to show
@@ -109,7 +110,6 @@ public class ListNotification<T> extends TemplateNotification {
        public void setElements(Collection<? extends T> elements) {
                this.elements.clear();
                this.elements.addAll(elements);
-               touch();
        }
 
        /**
index 2a98ec8..2362d6a 100644 (file)
@@ -24,7 +24,10 @@ import java.util.List;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.validation.Validation;
 
 /**
  * Filter for {@link ListNotification}s.
@@ -47,28 +50,25 @@ public class ListNotificationFilters {
         *            The current Sone, or {@code null} if not logged in
         * @return The filtered notifications
         */
-       public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
-               ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
-               if (newPostNotification != null) {
-                       ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
-                       int notificationIndex = notifications.indexOf(newPostNotification);
-                       if (filteredNotification == null) {
-                               notifications.remove(notificationIndex);
-                       } else {
-                               notifications.set(notificationIndex, filteredNotification);
-                       }
-               }
-               ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
-               if (newReplyNotification != null) {
-                       ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
-                       int notificationIndex = notifications.indexOf(newReplyNotification);
-                       if (filteredNotification == null) {
-                               notifications.remove(notificationIndex);
+       @SuppressWarnings("unchecked")
+       public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
+               List<Notification> filteredNotifications = new ArrayList<Notification>();
+               for (Notification notification : notifications) {
+                       if (notification.getId().equals("new-post-notification")) {
+                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
+                       } else if (notification.getId().equals("new-replies-notification")) {
+                               ListNotification<Reply> filteredNotification = filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
                        } else {
-                               notifications.set(notificationIndex, filteredNotification);
+                               filteredNotifications.add(notification);
                        }
                }
-               return notifications;
+               return filteredNotifications;
        }
 
        /**
@@ -84,13 +84,13 @@ public class ListNotificationFilters {
         * @return The filtered new-post notification, or {@code null} if the
         *         notification should be removed
         */
-       private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
+       public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
                if (currentSone == null) {
                        return null;
                }
                List<Post> newPosts = new ArrayList<Post>();
                for (Post post : newPostNotification.getElements()) {
-                       if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+                       if (isPostVisible(currentSone, post)) {
                                newPosts.add(post);
                        }
                }
@@ -119,13 +119,13 @@ public class ListNotificationFilters {
         * @return The filtered new-reply notification, or {@code null} if the
         *         notification should be removed
         */
-       private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+       public static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
                if (currentSone == null) {
                        return null;
                }
                List<Reply> newReplies = new ArrayList<Reply>();
                for (Reply reply : newReplyNotification.getElements()) {
-                       if (currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient())) {
+                       if (isReplyVisible(currentSone, reply)) {
                                newReplies.add(reply);
                        }
                }
@@ -141,29 +141,94 @@ public class ListNotificationFilters {
        }
 
        /**
-        * Finds the notification with the given ID in the list of notifications and
-        * returns it.
+        * Checks whether a post is visible to the given Sone. A post is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The post does not have a Sone.</li>
+        * <li>The Sone of the post is not the given Sone, the given Sone does not
+        * follow the post’s Sone, and the given Sone is not the recipient of the
+        * post.</li>
+        * <li>The trust relationship between the two Sones can not be retrieved.</li>
+        * <li>The given Sone has explicitely assigned negative trust to the post’s
+        * Sone.</li>
+        * <li>The given Sone has not explicitely assigned negative trust to the
+        * post’s Sone but the implicit trust is negative.</li>
+        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+        * </ul>
+        * If none of these statements is true the post is considered visible.
         *
-        * @param <T>
-        *            The type of the item in the notification
-        * @param notifications
-        *            The notification to search
-        * @param notificationId
-        *            The ID of the requested notification
-        * @param notificationElementClass
-        *            The class of the notification item
-        * @return The requested notification, or {@code null} if no notification
-        *         with the given ID could be found
+        * @param sone
+        *            The Sone that checks for a post’s visibility
+        * @param post
+        *            The post to check for visibility
+        * @return {@code true} if the post is considered visible, {@code false}
+        *         otherwise
         */
-       @SuppressWarnings("unchecked")
-       private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
-               for (Notification notification : notifications) {
-                       if (!notificationId.equals(notification.getId())) {
-                               continue;
+       public static boolean isPostVisible(Sone sone, Post post) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Post", post).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+               Sone postSone = post.getSone();
+               if (postSone == null) {
+                       return false;
+               }
+               Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+               if (trust != null) {
+                       if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+                               return false;
+                       }
+                       if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+                               return false;
                        }
-                       return (ListNotification<T>) notification;
+               } else {
+                       return false;
+               }
+               if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
+                       return false;
+               }
+               if (post.getTime() > System.currentTimeMillis()) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Checks whether a reply is visible to the given Sone. A reply is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The reply does not have a post.</li>
+        * <li>The reply’s post does not have a Sone.</li>
+        * <li>The Sone of the reply’s post is not the given Sone, the given Sone
+        * does not follow the reply’s post’s Sone, and the given Sone is not the
+        * recipient of the reply’s post.</li>
+        * <li>The trust relationship between the two Sones can not be retrieved.</li>
+        * <li>The given Sone has explicitely assigned negative trust to the post’s
+        * Sone.</li>
+        * <li>The given Sone has not explicitely assigned negative trust to the
+        * reply’s post’s Sone but the implicit trust is negative.</li>
+        * <li>The reply’s post’s {@link Post#getTime() time} is in the future.</li>
+        * <li>The reply’s {@link Reply#getTime() time} is in the future.</li>
+        * </ul>
+        * If none of these statements is true the reply is considered visible.
+        *
+        * @param sone
+        *            The Sone that checks for a post’s visibility
+        * @param reply
+        *            The reply to check for visibility
+        * @return {@code true} if the reply is considered visible, {@code false}
+        *         otherwise
+        */
+       public static boolean isReplyVisible(Sone sone, Reply reply) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Reply", reply).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+               Post post = reply.getPost();
+               if (post == null) {
+                       return false;
+               }
+               if (!isPostVisible(sone, post)) {
+                       return false;
+               }
+               if (reply.getTime() > System.currentTimeMillis()) {
+                       return false;
                }
-               return null;
+               return true;
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java b/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java
deleted file mode 100644 (file)
index c4468ea..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Sone - NotificationManagerAccessor.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
- * 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.template;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * Adds additional properties to a {@link NotificationManager}.
- * <dl>
- * <dd>all</dd>
- * <dt>Returns all notifications, sorted by creation time, oldest first.</dt>
- * <dd>new</dd>
- * <dt>Returns all changed notifications, sorted by last updated time, newest
- * first.</dt>
- * </dl>
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NotificationManagerAccessor extends ReflectionAccessor {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Object get(TemplateContext templateContext, Object object, String member) {
-               NotificationManager notificationManager = (NotificationManager) object;
-               if ("all".equals(member)) {
-                       List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
-                       Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
-                       return notifications;
-               }
-               return super.get(templateContext, object, member);
-       }
-
-}
index 477e972..db945b5 100644 (file)
@@ -25,6 +25,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
 import net.pterodactylus.sone.text.FreenetLinkParserContext;
+import net.pterodactylus.sone.web.page.Page.Request;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateContextFactory;
@@ -70,7 +71,7 @@ public class ParserFilter implements Filter {
                if (sone == null) {
                        sone = core.getSone(soneKey, false);
                }
-               FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
+               FreenetLinkParserContext context = new FreenetLinkParserContext((Request) templateContext.get("request"), sone);
                try {
                        return linkParser.parse(context, new StringReader(text));
                } catch (IOException ioe1) {
index 23b2227..76fdcc1 100644 (file)
@@ -25,6 +25,8 @@ import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -97,6 +99,8 @@ public class SoneAccessor extends ReflectionAccessor {
                        return core.isNewSone(sone.getId());
                } else if (member.equals("locked")) {
                        return core.isLocked(sone);
+               } else if (member.equals("lastUpdatedText")) {
+                       return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), System.currentTimeMillis() - sone.getTime());
                } else if (member.equals("trust")) {
                        Sone currentSone = (Sone) templateContext.get("currentSone");
                        if (currentSone == null) {
index cea4452..7cb44ce 100644 (file)
@@ -253,7 +253,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                        } else if (linkType == LinkType.POST) {
                                                String postId = link.substring(7);
                                                Post post = core.getPost(postId, false);
-                                               if (post != null) {
+                                               if ((post != null) && (post.getSone() != null)) {
                                                        String postText = post.getText();
                                                        postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
                                                        Sone postSone = post.getSone();
index e524d91..0712fc0 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.text;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request;
 
 /**
  * {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
@@ -28,20 +29,35 @@ import net.pterodactylus.sone.data.Sone;
  */
 public class FreenetLinkParserContext implements ParserContext {
 
+       /** The request being processed. */
+       private final Request request;
+
        /** The posting Sone. */
        private final Sone postingSone;
 
        /**
         * Creates a new link parser context.
         *
+        * @param request
+        *            The request being processed
         * @param postingSone
         *            The posting Sone
         */
-       public FreenetLinkParserContext(Sone postingSone) {
+       public FreenetLinkParserContext(Request request, Sone postingSone) {
+               this.request = request;
                this.postingSone = postingSone;
        }
 
        /**
+        * Returns the request that is currently being processed.
+        *
+        * @return The request being processed
+        */
+       public Request getRequest() {
+               return request;
+       }
+
+       /**
         * Returns the Sone that provided the text that is being parsed.
         *
         * @return The posting Sone
index 7f38ad8..ee61b6f 100644 (file)
@@ -1,3 +1,19 @@
+/*
+ * Sone - ParserContext.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
+ * 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.text;
 
diff --git a/src/main/java/net/pterodactylus/sone/text/TextFilter.java b/src/main/java/net/pterodactylus/sone/text/TextFilter.java
new file mode 100644 (file)
index 0000000..5744368
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sone - TextFilter.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.text;
+
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Filter for newly inserted text. This filter strips HTTP links to the local
+ * node of identifying marks, e.g. a link to “http://localhost:8888/KSK@gpl.txt”
+ * will be converted to “KSK@gpl.txt”. This will only work for links that point
+ * to the same address Sone is accessed by, so if you access Sone using
+ * localhost:8888, links to 127.0.0.1:8888 will not be removed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TextFilter {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(TextFilter.class);
+
+       /**
+        * Filters the given text, stripping the host header part for links to the
+        * local node.
+        *
+        * @param hostHeader
+        *            The host header from the request
+        * @param text
+        *            The text to filter
+        * @return The filtered text
+        */
+       public static String filter(String hostHeader, String text) {
+
+               /* filter http(s) links to own node. */
+               if (hostHeader != null) {
+                       String line = text;
+                       for (String toRemove : new String[] { "http://" + hostHeader + "/", "https://" + hostHeader + "/", "http://" + hostHeader, "https://" + hostHeader }) {
+                               while (line.indexOf(toRemove) != -1) {
+                                       line = line.replace(toRemove, "");
+                               }
+                       }
+                       return line;
+               }
+
+               /* not modified. */
+               return text;
+       }
+
+}
index 57eb18a..147e4ae 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -64,6 +65,7 @@ public class CreatePostPage extends SoneTemplatePage {
                                        sender = currentSone;
                                }
                                Sone recipient = webInterface.getCore().getSone(recipientId, false);
+                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                                webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text);
                                throw new RedirectException(returnPage);
                        }
index cc40320..3f940a3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - CreateSonePage.java - Copyright © 2010 David Roden
+ * Sone - CreateSonePage.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
@@ -129,6 +129,9 @@ public class CreateSonePage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
        }
 
index 54d77b8..21979b8 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - DeleteSonePage.java - Copyright © 2010 David Roden
+ * Sone - DeleteSonePage.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
index c055545..37a792c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustPage.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
index e675311..35b88a9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - IndexPage.java - Copyright © 2010 David Roden
+ * Sone - IndexPage.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
@@ -23,7 +23,9 @@ import java.util.List;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -57,7 +59,7 @@ public class IndexPage extends SoneTemplatePage {
        @Override
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
-               Sone currentSone = getCurrentSone(request.getToadletContext());
+               final Sone currentSone = getCurrentSone(request.getToadletContext());
                List<Post> allPosts = new ArrayList<Post>();
                allPosts.addAll(currentSone.getPosts());
                for (String friendSoneId : currentSone.getFriends()) {
@@ -73,6 +75,13 @@ public class IndexPage extends SoneTemplatePage {
                                }
                        }
                }
+               allPosts = Filters.filteredList(allPosts, new Filter<Post>() {
+
+                       @Override
+                       public boolean filterObject(Post post) {
+                               return ListNotificationFilters.isPostVisible(currentSone, post);
+                       }
+               });
                allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
                Collections.sort(allPosts, Post.TIME_COMPARATOR);
                Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
index 73fdd3c..56f55ef 100644 (file)
@@ -53,7 +53,7 @@ public class LikePage extends SoneTemplatePage {
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
index eaa8351..8e612ea 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - LoginPage.java - Copyright © 2010 David Roden
+ * Sone - LoginPage.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
@@ -103,6 +103,9 @@ public class LoginPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return getCurrentSone(toadletContext, false) == null;
        }
 
index f388368..7cd0587 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - LogoutPage.java - Copyright © 2010 David Roden
+ * Sone - LogoutPage.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
@@ -57,6 +57,9 @@ public class LogoutPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return (getCurrentSone(toadletContext, false) != null) && (webInterface.getCore().getLocalSones().size() != 1);
        }
 
index 4ecf6e0..9f91c84 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MarkReadPage.java - Copyright © 2011 David Roden
+ * Sone - MarkAsKnownPage.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
index 161de32..6785e81 100644 (file)
@@ -17,6 +17,9 @@
 
 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;
@@ -57,19 +60,38 @@ public class OptionsPage extends SoneTemplatePage {
                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;
@@ -87,13 +109,17 @@ public class OptionsPage extends SoneTemplatePage {
                        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());
index 1d1aa36..6f5e001 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - OptionsPage.java - Copyright © 2010 David Roden
+ * Sone - SearchPage.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,17 +24,20 @@ import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 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.util.collection.Converter;
-import net.pterodactylus.util.collection.Converters;
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -49,6 +52,9 @@ import net.pterodactylus.util.text.TextException;
  */
 public class SearchPage extends SoneTemplatePage {
 
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(SearchPage.class);
+
        /**
         * Creates a new search page.
         *
@@ -99,8 +105,8 @@ public class SearchPage extends SoneTemplatePage {
                Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR);
 
                /* extract Sones and posts. */
-               List<Sone> resultSones = Converters.convertList(sortedSoneHits, new HitConverter<Sone>());
-               List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
+               List<Sone> resultSones = Mappers.mappedList(sortedSoneHits, new HitMapper<Sone>());
+               List<Post> resultPosts = Mappers.mappedList(sortedPostHits, new HitMapper<Post>());
 
                /* pagination. */
                Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
@@ -137,7 +143,7 @@ public class SearchPage extends SoneTemplatePage {
                Set<Hit<T>> hits = new HashSet<Hit<T>>();
                for (T object : objects) {
                        String objectString = stringGenerator.generateString(object);
-                       int score = calculateScore(phrases, objectString);
+                       double score = calculateScore(phrases, objectString);
                        hits.add(new Hit<T>(object, score));
                }
                return hits;
@@ -185,9 +191,10 @@ public class SearchPage extends SoneTemplatePage {
         *            The expression to search
         * @return The score of the expression
         */
-       private int calculateScore(List<Phrase> phrases, String expression) {
-               int optionalHits = 0;
-               int requiredHits = 0;
+       private double calculateScore(List<Phrase> phrases, String expression) {
+               logger.log(Level.FINEST, "Calculating Score for “%s”…", expression);
+               double optionalHits = 0;
+               double requiredHits = 0;
                int forbiddenHits = 0;
                int requiredPhrases = 0;
                for (Phrase phrase : phrases) {
@@ -197,22 +204,26 @@ public class SearchPage extends SoneTemplatePage {
                        }
                        int matches = 0;
                        int index = 0;
+                       double score = 0;
                        while (index < expression.length()) {
                                int position = expression.toLowerCase().indexOf(phraseString, index);
                                if (position == -1) {
                                        break;
                                }
+                               score += Math.pow(1 - position / (double) expression.length(), 2);
                                index = position + phraseString.length();
+                               logger.log(Level.FINEST, "Got hit at position %d.", position);
                                ++matches;
                        }
+                       logger.log(Level.FINEST, "Score: %f", score);
                        if (matches == 0) {
                                continue;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
-                               requiredHits += matches;
+                               requiredHits += score;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
-                               optionalHits += matches;
+                               optionalHits += score;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
                                forbiddenHits += matches;
@@ -418,7 +429,7 @@ public class SearchPage extends SoneTemplatePage {
 
                        @Override
                        public int compare(Hit<?> leftHit, Hit<?> rightHit) {
-                               return rightHit.getScore() - leftHit.getScore();
+                               return (rightHit.getScore() < leftHit.getScore()) ? -1 : ((rightHit.getScore() > leftHit.getScore()) ? 1 : 0);
                        }
 
                };
@@ -427,7 +438,7 @@ public class SearchPage extends SoneTemplatePage {
                private final T object;
 
                /** The score of the object. */
-               private final int score;
+               private final double score;
 
                /**
                 * Creates a new hit.
@@ -437,7 +448,7 @@ public class SearchPage extends SoneTemplatePage {
                 * @param score
                 *            The score of the object
                 */
-               public Hit(T object, int score) {
+               public Hit(T object, double score) {
                        this.object = object;
                        this.score = score;
                }
@@ -456,7 +467,7 @@ public class SearchPage extends SoneTemplatePage {
                 *
                 * @return The score of the object
                 */
-               public int getScore() {
+               public double getScore() {
                        return score;
                }
 
@@ -469,13 +480,13 @@ public class SearchPage extends SoneTemplatePage {
         *            The type of the object to extract
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       public static class HitConverter<T> implements Converter<Hit<T>, T> {
+       public static class HitMapper<T> implements Mapper<Hit<T>, T> {
 
                /**
                 * {@inheritDoc}
                 */
                @Override
-               public T convert(Hit<T> input) {
+               public T map(Hit<T> input) {
                        return input.getObject();
                }
 
index d6c41c1..42c0129 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Freetalk - FreetalkTemplatePage.java - Copyright © 2010 David Roden
+ * Sone - SoneTemplatePage.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
@@ -26,8 +26,9 @@ import java.util.Map;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.web.page.FreenetTemplatePage;
+import net.pterodactylus.sone.web.page.Page;
 import net.pterodactylus.util.collection.ListBuilder;
 import net.pterodactylus.util.collection.MapBuilder;
 import net.pterodactylus.util.template.Template;
@@ -37,7 +38,7 @@ import freenet.clients.http.ToadletContext;
 import freenet.support.api.HTTPRequest;
 
 /**
- * Base page for the Freetalk web interface.
+ * Base page for the Sone web interface.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -53,8 +54,8 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        private final boolean requireLogin;
 
        /**
-        * Creates a new template page for Freetalk that does not require the user
-        * to be logged in.
+        * Creates a new template page for Sone that does not require the user to be
+        * logged in.
         *
         * @param path
         *            The path of the page
@@ -68,8 +69,8 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk that does not require the user
-        * to be logged in.
+        * Creates a new template page for Sone that does not require the user to be
+        * logged in.
         *
         * @param path
         *            The path of the page
@@ -85,7 +86,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk.
+        * Creates a new template page for Sone.
         *
         * @param path
         *            The path of the page
@@ -101,7 +102,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk.
+        * Creates a new template page for Sone.
         *
         * @param path
         *            The path of the page
@@ -249,7 +250,8 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        @Override
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
-               templateContext.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               templateContext.set("currentSone", currentSone);
                templateContext.set("localSones", webInterface.getCore().getLocalSones());
                templateContext.set("request", request);
                templateContext.set("currentVersion", SonePlugin.VERSION);
@@ -257,6 +259,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
                templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
                templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
                templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+               templateContext.set("notifications", ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone));
        }
 
        /**
@@ -293,7 +296,18 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
+       protected boolean isFullAccessOnly() {
+               return webInterface.getCore().getPreferences().isRequireFullAccess();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                if (requiresLogin()) {
                        return getCurrentSone(toadletContext, false) != null;
                }
index 85f46f0..60165b2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkPage.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
index e026a9a..a7836f5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FollowSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnfollowSonePage.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
index 5408f20..ccb5959 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LockSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSonePage.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
index 0e7e198..c5e7dec 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustPage.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
index 14c4f94..889c74c 100644 (file)
@@ -80,6 +80,10 @@ public class ViewSonePage extends SoneTemplatePage {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
                templateContext.set("sone", sone);
+               templateContext.set("soneId", soneId);
+               if (sone == null) {
+                       return;
+               }
                List<Post> sonePosts = sone.getPosts();
                sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
                Collections.sort(sonePosts, Post.TIME_COMPARATOR);
index 46592d8..75caba2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ * Sone - WebInterface.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
@@ -49,7 +49,6 @@ import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.HttpRequestAccessor;
 import net.pterodactylus.sone.template.IdentityAccessor;
 import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
@@ -70,6 +69,7 @@ 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.GetNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
@@ -87,6 +87,7 @@ 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.RedirectPage;
 import net.pterodactylus.sone.web.page.StaticPage;
 import net.pterodactylus.sone.web.page.TemplatePage;
 import net.pterodactylus.util.cache.Cache;
@@ -100,6 +101,7 @@ import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
 import net.pterodactylus.util.template.CollectionSortFilter;
+import net.pterodactylus.util.template.ContainsFilter;
 import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
@@ -191,7 +193,6 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
-               templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
                templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
                templateContextFactory.addFilter("date", new DateFilter());
@@ -210,6 +211,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
                templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
+               templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("formPassword", formPassword);
@@ -544,6 +546,7 @@ public class WebInterface implements CoreListener {
                Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
@@ -584,6 +587,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
index eff7c41..0fc1236 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -59,6 +60,7 @@ public class CreatePostAjaxPage extends JsonPage {
                if ((text == null) || (text.trim().length() == 0)) {
                        return createErrorJsonObject("text-required");
                }
+               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                Post newPost = webInterface.getCore().createPost(sender, recipient, text);
                return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", (newPost.getRecipient() != null) ? newPost.getRecipient().getId() : null);
        }
index f34d202..e12b2cf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - DeleteReplysAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - DeleteReplyAjaxPage.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
index 1a6d28f..ca770e4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustAjaxPage.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
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
new file mode 100644 (file)
index 0000000..650a9ae
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Sone - GetNotificationAjaxPage.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
+ * 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.ajax;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The “get notification” AJAX handler returns a number of rendered
+ * notifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetNotificationAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “get notification” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetNotificationAjaxPage(WebInterface webInterface) {
+               super("getNotification.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean needsFormPassword() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       @SuppressWarnings("unchecked")
+       protected JsonObject createJsonObject(Request request) {
+               String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
+               JsonObject jsonNotifications = new JsonObject();
+               Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
+               for (String notificationId : notificationIds) {
+                       Notification notification = webInterface.getNotifications().getNotification(notificationId);
+                       ListNotificationFilters.filterNotifications(new ArrayList<Notification>(), currentSone);
+                       if ("new-post-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+                       } else if ("new-reply-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+                       }
+                       if (notification == null) {
+                               // TODO - show error
+                               continue;
+                       }
+                       jsonNotifications.put(notificationId, createJsonNotification(request, notification));
+               }
+               return createSuccessJsonObject().put("notifications", jsonNotifications);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Creates a JSON object from the given notification.
+        *
+        * @param request
+        *            The request to load the session from
+        * @param notification
+        *            The notification to create a JSON object
+        * @return The JSON object
+        */
+       private JsonObject createJsonNotification(Request request, Notification notification) {
+               JsonObject jsonNotification = new JsonObject();
+               jsonNotification.put("id", notification.getId());
+               StringWriter notificationWriter = new StringWriter();
+               try {
+                       if (notification instanceof TemplateNotification) {
+                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+                               templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
+                               templateContext.set("localSones", webInterface.getCore().getLocalSones());
+                               templateContext.set("request", request);
+                               templateContext.set("currentVersion", SonePlugin.VERSION);
+                               templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+                               templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+                               templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+                               templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+                               templateContext.set("notification", notification);
+                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
+                       } else {
+                               notification.render(notificationWriter);
+                       }
+               } catch (IOException ioe1) {
+                       /* StringWriter never throws, ignore. */
+               }
+               jsonNotification.put("text", notificationWriter.toString());
+               jsonNotification.put("createdTime", notification.getCreatedTime());
+               jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
+               jsonNotification.put("dismissable", notification.isDismissable());
+               return jsonNotification;
+       }
+
+}
index 4e5f1b8..f46ec6b 100644 (file)
@@ -62,7 +62,7 @@ public class GetPostAjaxPage extends JsonPage {
                if (post == null) {
                        return createErrorJsonObject("invalid-post-id");
                }
-               return createSuccessJsonObject().put("post", createJsonPost(post, getCurrentSone(request.getToadletContext())));
+               return createSuccessJsonObject().put("post", createJsonPost(request, post, getCurrentSone(request.getToadletContext())));
        }
 
        /**
@@ -81,13 +81,15 @@ public class GetPostAjaxPage extends JsonPage {
         * Creates a JSON object from the given post. The JSON object will only
         * contain the ID of the post, its time, and its rendered HTML code.
         *
+        * @param request
+        *            The request being processed
         * @param post
         *            The post to create a JSON object from
         * @param currentSone
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the post
         */
-       private JsonObject createJsonPost(Post post, Sone currentSone) {
+       private JsonObject createJsonPost(Request request, Post post, Sone currentSone) {
                JsonObject jsonPost = new JsonObject();
                jsonPost.put("id", post.getId());
                jsonPost.put("sone", post.getSone().getId());
@@ -95,6 +97,7 @@ public class GetPostAjaxPage extends JsonPage {
                jsonPost.put("time", post.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("request", request);
                templateContext.set("post", post);
                templateContext.set("currentSone", currentSone);
                templateContext.set("localSones", webInterface.getCore().getLocalSones());
index c19bcba..24bbbe2 100644 (file)
@@ -65,7 +65,7 @@ public class GetReplyAjaxPage extends JsonPage {
                if ((reply == null) || (reply.getSone() == null)) {
                        return createErrorJsonObject("invalid-reply-id");
                }
-               return createSuccessJsonObject().put("reply", createJsonReply(reply, getCurrentSone(request.getToadletContext())));
+               return createSuccessJsonObject().put("reply", createJsonReply(request, reply, getCurrentSone(request.getToadletContext())));
        }
 
        /**
@@ -83,13 +83,15 @@ public class GetReplyAjaxPage extends JsonPage {
        /**
         * Creates a JSON representation of the given reply.
         *
+        * @param request
+        *            The request being processed
         * @param reply
         *            The reply to convert
         * @param currentSone
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the reply
         */
-       private JsonObject createJsonReply(Reply reply, Sone currentSone) {
+       private JsonObject createJsonReply(Request request, Reply reply, Sone currentSone) {
                JsonObject jsonReply = new JsonObject();
                jsonReply.put("id", reply.getId());
                jsonReply.put("postId", reply.getPost().getId());
@@ -97,6 +99,7 @@ public class GetReplyAjaxPage extends JsonPage {
                jsonReply.put("time", reply.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("request", request);
                templateContext.set("reply", reply);
                templateContext.set("currentSone", currentSone);
                try {
index 426c08f..4e2049e 100644 (file)
@@ -17,8 +17,6 @@
 
 package net.pterodactylus.sone.web.ajax;
 
-import java.io.IOException;
-import java.io.StringWriter;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -39,8 +37,6 @@ import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * The “get status” AJAX handler returns all information that is necessary to
@@ -70,10 +66,19 @@ public class GetStatusAjaxPage extends JsonPage {
        protected JsonObject createJsonObject(Request request) {
                final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                /* load Sones. */
-               boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
+               boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "false"));
                Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
                if (loadAllSones) {
                        sones.addAll(webInterface.getCore().getSones());
+               } else {
+                       String loadSoneIds = request.getHttpRequest().getParam("soneIds");
+                       if (loadSoneIds.length() > 0) {
+                               String[] soneIds = loadSoneIds.split(",");
+                               for (String soneId : soneIds) {
+                                       /* just add it, we skip null further down. */
+                                       sones.add(webInterface.getCore().getSone(soneId, false));
+                               }
+                       }
                }
                JsonArray jsonSones = new JsonArray();
                for (Sone sone : sones) {
@@ -86,9 +91,9 @@ public class GetStatusAjaxPage extends JsonPage {
                /* load notifications. */
                List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
                Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
-               JsonArray jsonNotifications = new JsonArray();
+               JsonArray jsonNotificationInformations = new JsonArray();
                for (Notification notification : notifications) {
-                       jsonNotifications.add(createJsonNotification(notification));
+                       jsonNotificationInformations.add(createJsonNotificationInformation(notification));
                }
                /* load new posts. */
                Set<Post> newPosts = webInterface.getNewPosts();
@@ -97,7 +102,7 @@ public class GetStatusAjaxPage extends JsonPage {
 
                                @Override
                                public boolean filterObject(Post post) {
-                                       return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+                                       return ListNotificationFilters.isPostVisible(currentSone, post);
                                }
 
                        });
@@ -118,7 +123,7 @@ public class GetStatusAjaxPage extends JsonPage {
 
                                @Override
                                public boolean filterObject(Reply reply) {
-                                       return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+                                       return ListNotificationFilters.isReplyVisible(currentSone, reply);
                                }
 
                        });
@@ -132,7 +137,7 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonReply.put("postSone", reply.getPost().getSone().getId());
                        jsonReplies.add(jsonReply);
                }
-               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
 
        /**
@@ -174,36 +179,25 @@ public class GetStatusAjaxPage extends JsonPage {
                synchronized (dateFormat) {
                        jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
                }
-               jsonSone.put("age", (System.currentTimeMillis() - sone.getTime()) / 1000);
+               jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, System.currentTimeMillis() - sone.getTime()).getText());
                return jsonSone;
        }
 
        /**
-        * Creates a JSON object from the given notification.
+        * Creates a JSON object that only contains the ID and the last-updated time
+        * of the given notification.
         *
+        * @see Notification#getId()
+        * @see Notification#getLastUpdatedTime()
         * @param notification
-        *            The notification to create a JSON object
-        * @return The JSON object
+        *            The notification
+        * @return A JSON object containing the notification ID and last-updated
+        *         time
         */
-       private JsonObject createJsonNotification(Notification notification) {
+       private JsonObject createJsonNotificationInformation(Notification notification) {
                JsonObject jsonNotification = new JsonObject();
                jsonNotification.put("id", notification.getId());
-               StringWriter notificationWriter = new StringWriter();
-               try {
-                       if (notification instanceof TemplateNotification) {
-                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
-                               templateContext.set("notification", notification);
-                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
-                       } else {
-                               notification.render(notificationWriter);
-                       }
-               } catch (IOException ioe1) {
-                       /* StringWriter never throws, ignore. */
-               }
-               jsonNotification.put("text", notificationWriter.toString());
-               jsonNotification.put("createdTime", notification.getCreatedTime());
                jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
-               jsonNotification.put("dismissable", notification.isDismissable());
                return jsonNotification;
        }
 
index b17e4a2..0be0c46 100644 (file)
@@ -120,6 +120,23 @@ public class GetTimesAjaxPage extends JsonPage {
         * @return The formatted age
         */
        private Time getTime(long age) {
+               return getTime(webInterface, age);
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Returns the formatted relative time for a given age.
+        *
+        * @param webInterface
+        *            The Sone web interface (for l10n access)
+        * @param age
+        *            The age to format (in milliseconds)
+        * @return The formatted age
+        */
+       public static Time getTime(WebInterface webInterface, long age) {
                String text;
                long refresh;
                if (age < 0) {
@@ -135,7 +152,7 @@ public class GetTimesAjaxPage extends JsonPage {
                        text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
                        refresh = Time.MINUTE;
                } else if (age < 30 * Time.MINUTE) {
-                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) (Digits.round(age, Time.MINUTE) / Time.MINUTE)));
                        refresh = 1 * Time.MINUTE;
                } else if (age < 45 * Time.MINUTE) {
                        text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
@@ -144,31 +161,31 @@ public class GetTimesAjaxPage extends JsonPage {
                        text = webInterface.getL10n().getString("View.Time.AnHourAgo");
                        refresh = Time.HOUR;
                } else if (age < 21 * Time.HOUR) {
-                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) (Digits.round(age, Time.HOUR) / Time.HOUR)));
                        refresh = Time.HOUR;
                } else if (age < 42 * Time.HOUR) {
                        text = webInterface.getL10n().getString("View.Time.ADayAgo");
                        refresh = Time.DAY;
                } else if (age < 6 * Time.DAY) {
-                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) (Digits.round(age, Time.DAY) / Time.DAY)));
                        refresh = Time.DAY;
                } else if (age < 11 * Time.DAY) {
                        text = webInterface.getL10n().getString("View.Time.AWeekAgo");
                        refresh = Time.DAY;
                } else if (age < 4 * Time.WEEK) {
-                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) (Digits.round(age, Time.WEEK) / Time.WEEK)));
                        refresh = Time.DAY;
                } else if (age < 6 * Time.WEEK) {
                        text = webInterface.getL10n().getString("View.Time.AMonthAgo");
                        refresh = Time.DAY;
                } else if (age < 11 * Time.MONTH) {
-                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) (Digits.round(age, Time.MONTH) / Time.MONTH)));
                        refresh = Time.DAY;
                } else if (age < 18 * Time.MONTH) {
                        text = webInterface.getL10n().getString("View.Time.AYearAgo");
                        refresh = Time.WEEK;
                } else {
-                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) (Digits.round(age, Time.YEAR) / Time.YEAR)));
                        refresh = Time.WEEK;
                }
                return new Time(text, refresh);
@@ -179,7 +196,7 @@ public class GetTimesAjaxPage extends JsonPage {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private static class Time {
+       public static class Time {
 
                /** Number of milliseconds in a second. */
                private static final long SECOND = 1000;
@@ -239,6 +256,14 @@ public class GetTimesAjaxPage extends JsonPage {
                        return refresh;
                }
 
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String toString() {
+                       return text;
+               }
+
        }
 
 }
index 8d48bce..b027ab8 100644 (file)
@@ -188,15 +188,18 @@ public abstract class JsonPage implements Page {
         */
        @Override
        public Response handleRequest(Request request) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
+                       return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+               }
                if (needsFormPassword()) {
                        String formPassword = request.getHttpRequest().getParam("formPassword");
                        if (!webInterface.getFormPassword().equals(formPassword)) {
-                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+                               return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
                if (requiresLogin()) {
                        if (getCurrentSone(request.getToadletContext(), false) == null) {
-                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+                               return new Response(403, "Forbidden", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
                        }
                }
                JsonObject jsonObject = createJsonObject(request);
index ad1689e..f07dcb0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkAjaxPage.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
index e1c408c..3160db0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LockSoneAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSoneAjaxPage.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
index 32c6229..ad613ff 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustAjaxPage.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
index 0668154..5831a1b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - TemplatePage.java - Copyright © 2010 David Roden
+ * Sone - FreenetTemplatePage.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
@@ -109,6 +109,9 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
                        return new RedirectResponse(redirectTarget);
                }
 
+               if (isFullAccessOnly() && !request.getToadletContext().isAllowedFullAccess()) {
+                       return new Response(401, "Not authorized", "text/html", "Not authorized");
+               }
                ToadletContext toadletContext = request.getToadletContext();
                if (request.getMethod() == Method.POST) {
                        /* require form password. */
@@ -227,6 +230,17 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
                return Collections.emptyList();
        }
 
+       /**
+        * Returns whether this page should only be allowed for requests from hosts
+        * with full access.
+        *
+        * @return {@code true} if this page should only be allowed for hosts with
+        *         full access, {@code false} to allow this page for any host
+        */
+       protected boolean isFullAccessOnly() {
+               return false;
+       }
+
        //
        // INTERFACE LinkEnabledCallback
        //
@@ -236,7 +250,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
-               return true;
+               return !isFullAccessOnly();
        }
 
        /**
index dd54f41..297081e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - Page.java - Copyright © 2010 David Roden
+ * Sone - Page.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
index f7f59a6..f594e58 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - PageToadlet.java - Copyright © 2010 David Roden
+ * Sone - PageToadlet.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
index af87470..bd0adb9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - PageToadletFactory.java - Copyright © 2010 David Roden
+ * Sone - PageToadletFactory.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
diff --git a/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java b/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java
new file mode 100644 (file)
index 0000000..2ce34f9
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Sone - RedirectPage.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.page;
+
+/**
+ * Page implementation that redirects the user to another URL.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RedirectPage implements Page {
+
+       /** The original path. */
+       private String originalPath;
+
+       /** The path to redirect the browser to. */
+       private String newPath;
+
+       /**
+        * Creates a new redirect page.
+        *
+        * @param originalPath
+        *            The original path
+        * @param newPath
+        *            The path to redirect the browser to
+        */
+       public RedirectPage(String originalPath, String newPath) {
+               this.originalPath = originalPath;
+               this.newPath = newPath;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return originalPath;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(Request request) {
+               return new RedirectResponse(newPath);
+       }
+
+}
index a840958..3d24fdd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - CSSPage.java - Copyright © 2010 David Roden
+ * Sone - StaticPage.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
index ba38c06..5032867 100644 (file)
@@ -33,10 +33,12 @@ Page.Options.Page.Title=Options
 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.
@@ -48,10 +50,13 @@ 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
@@ -139,7 +144,8 @@ 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.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,6 +235,8 @@ View.Sone.Status.Downloading=This Sone is currently being downloaded.
 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
@@ -256,7 +264,7 @@ View.Time.XHoursAgo=${hour} hours ago
 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
index f6bb62d..c0a7b6d 100644 (file)
@@ -89,10 +89,28 @@ textarea {
        content: '★ ';
 }
 
+#sone a.in-page-link:before {
+       content: '↓ ';
+}
+
 #sone a img {
        border: none;
 }
 
+#sone #main.offline {
+       opacity: 0.5;
+}
+
+#sone #offline-marker {
+       display: none;
+       position: fixed;
+       top: 2em;
+       right: 2em;
+       width: 128px;
+       height: 128px;
+       background-image: url("../images/sone-offline.png");
+}
+
 #sone #notification-area {
        margin-top: 1em;
 }
@@ -266,6 +284,10 @@ textarea {
        display: inline;
 }
 
+#sone .permalink {
+       display: inline;
+}
+
 #sone .post .bookmarks {
        display: inline;
        color: rgb(28, 131, 191);
@@ -648,3 +670,8 @@ textarea {
        font-weight: bold;
        color: red;
 }
+
+#sone .warning {
+       color: red;
+       font-style: italic;
+}
diff --git a/src/main/resources/static/images/sone-offline.png b/src/main/resources/static/images/sone-offline.png
new file mode 100644 (file)
index 0000000..18430bf
Binary files /dev/null and b/src/main/resources/static/images/sone-offline.png differ
index d60ee23..9c082f1 100644 (file)
@@ -1,26 +1,20 @@
 /* Sone JavaScript functions. */
 
-/* jQuery overrides. */
-oldGetJson = jQuery.prototype.getJSON;
-jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
-       if (typeof errorCallback == "undefined") {
-               return oldGetJson(url, data, successCallback);
-       }
-       if (jQuery.isFunction(data)) {
-               errorCallback = successCallback;
-               successCallback = data;
-               data = null;
-       }
-       return jQuery.ajax({
-               data: data,
-               error: errorCallback,
-               success: successCallback,
-               url: url
-       });
-}
-
-function isOnline() {
-       return $("#sone").hasClass("online");
+function ajaxGet(url, data, successCallback, errorCallback) {
+       (function(url, data, successCallback, errorCallback) {
+               $.ajax({"type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+                       ajaxSuccess();
+                       if (typeof successCallback != "undefined") {
+                               successCallback(data, textStatus);
+                       }
+               }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+                       if (typeof errorCallback != "undefined") {
+                               errorCallback();
+                       } else {
+                               ajaxError();
+                       }
+               }});
+       })(url, data, successCallback, errorCallback);
 }
 
 function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
@@ -33,7 +27,7 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
                                inputField.val(defaultText);
                        }
                }).hide().data("inputField", $(this)).val($(this).val());
-               $(this).after(textarea);
+               $(this).data("textarea", textarea).after(textarea);
                (function(inputField, textarea) {
                        inputField.focus(function() {
                                $(this).hide().attr("disabled", "disabled");
@@ -66,11 +60,11 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
  * @param element
  *            The element to add a “comment” link to
  */
-function addCommentLink(postId, element, insertAfterThisElement) {
+function addCommentLink(postId, author, element, insertAfterThisElement) {
        if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) {
                return;
        }
-       commentElement = (function(postId) {
+       commentElement = (function(postId, author) {
                separator = $("<span> · </span>").addClass("separator");
                var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
                        replyElement = $("#sone .post#" + postId + " .create-reply");
@@ -85,10 +79,11 @@ function addCommentLink(postId, element, insertAfterThisElement) {
                                        replyElement.removeClass("light");
                                });
                        })(replyElement);
-                       replyElement.find("input.reply-input").focus();
+                       textArea = replyElement.find("input.reply-input").focus().data("textarea");
+                       textArea.val(textArea.val() + "@sone://" + author + " ");
                });
                return commentElement;
-       })(postId);
+       })(postId, author);
        $(insertAfterThisElement).after(commentElement.clone(true));
        $(insertAfterThisElement).after(separator);
 }
@@ -109,13 +104,11 @@ function getTranslation(key, callback) {
                callback(translations[key]);
                return;
        }
-       $.getJSON("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+       ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        translations[key] = data.value;
                        callback(data.value);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* ignore error. */
        });
 }
 
@@ -145,7 +138,7 @@ function filterSoneId(soneId) {
  * @param lastUpdated
  *            The date and time of the last update (formatted for display)
  */
-function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
+function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
        $("#sone .sone." + filterSoneId(soneId)).
                toggleClass("unknown", status == "unknown").
                toggleClass("idle", status == "idle").
@@ -155,7 +148,7 @@ function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
        $("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
        $("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
        if (lastUpdated != null) {
-               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
        } else {
                getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
                        $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
@@ -211,7 +204,7 @@ function enhanceDeleteButton(button, text, deleteCallback) {
  */
 function enhanceDeletePostButton(button, postId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+               ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
@@ -243,7 +236,7 @@ function enhanceDeletePostButton(button, postId, text) {
  */
 function enhanceDeleteReplyButton(button, replyId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
+               ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
@@ -276,7 +269,7 @@ function getFormPassword() {
  */
 function getSone(soneId) {
        return $("#sone .sone").filter(function(index) {
-               return $(".id").text() == soneId;
+               return $(".id", this).text() == soneId;
        });
 }
 
@@ -415,8 +408,19 @@ function getNotificationId(notificationElement) {
        return $(notificationElement).attr("id");
 }
 
+/**
+ * Returns the time the notification was last updated.
+ *
+ * @param notificationElement
+ *            The notification element
+ * @returns The last update time of the notification
+ */
+function getNotificationLastUpdatedTime(notificationElement) {
+       return $(notificationElement).attr("lastUpdatedTime");
+}
+
 function likePost(postId) {
-       $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -429,7 +433,7 @@ function likePost(postId) {
 }
 
 function unlikePost(postId) {
-       $.getJSON("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -442,9 +446,9 @@ function unlikePost(postId) {
 }
 
 function updatePostLikes(postId) {
-       $.getJSON("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
+                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
                        $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
                        $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
@@ -454,7 +458,7 @@ function updatePostLikes(postId) {
 }
 
 function likeReply(replyId) {
-       $.getJSON("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -467,7 +471,7 @@ function likeReply(replyId) {
 }
 
 function unlikeReply(replyId) {
-       $.getJSON("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -486,7 +490,7 @@ function unlikeReply(replyId) {
  *            The ID of the Sone to trust
  */
 function trustSone(soneId) {
-       $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -500,7 +504,7 @@ function trustSone(soneId) {
  *            The ID of the Sone to distrust
  */
 function distrustSone(soneId) {
-       $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -514,7 +518,7 @@ function distrustSone(soneId) {
  *            The ID of the Sone to untrust
  */
 function untrustSone(soneId) {
-       $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -555,7 +559,7 @@ function updateTrustControls(soneId, trustValue) {
  */
 function bookmarkPost(postId) {
        (function(postId) {
-               $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                        if ((data != null) && data.success) {
                                getPost(postId).find(".bookmark").toggleClass("hidden", true);
                                getPost(postId).find(".unbookmark").toggleClass("hidden", false);
@@ -571,7 +575,7 @@ function bookmarkPost(postId) {
  *            The ID of the post to unbookmark
  */
 function unbookmarkPost(postId) {
-       $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+       ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        getPost(postId).find(".bookmark").toggleClass("hidden", false);
                        getPost(postId).find(".unbookmark").toggleClass("hidden", true);
@@ -580,9 +584,9 @@ function unbookmarkPost(postId) {
 }
 
 function updateReplyLikes(replyId) {
-       $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
+                       $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
                        $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
                        $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
@@ -605,7 +609,7 @@ function updateReplyLikes(replyId) {
  *            parameters: success, error, replyId)
  */
 function postReply(sender, postId, text, callbackFunction) {
-       $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+       ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
                if (data == null) {
                        /* TODO - show error */
                        return;
@@ -633,7 +637,7 @@ function ajaxifySone(soneElement) {
         */
        $(".follow", soneElement).submit(function() {
                var followElement = this;
-               $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(followElement).addClass("hidden");
                        $(followElement).parent().find(".unfollow").removeClass("hidden");
                });
@@ -641,7 +645,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unfollow", soneElement).submit(function() {
                var unfollowElement = this;
-               $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(unfollowElement).addClass("hidden");
                        $(unfollowElement).parent().find(".follow").removeClass("hidden");
                });
@@ -649,7 +653,7 @@ function ajaxifySone(soneElement) {
        });
        $(".lock", soneElement).submit(function() {
                var lockElement = this;
-               $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(lockElement).addClass("hidden");
                        $(lockElement).parent().find(".unlock").removeClass("hidden");
                });
@@ -657,7 +661,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unlock", soneElement).submit(function() {
                var unlockElement = this;
-               $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(unlockElement).addClass("hidden");
                        $(unlockElement).parent().find(".lock").removeClass("hidden");
                });
@@ -666,7 +670,7 @@ function ajaxifySone(soneElement) {
 
        /* mark Sone as known when clicking it. */
        $(soneElement).click(function() {
-               markSoneAsKnown(soneElement);
+               markSoneAsKnown(this);
        });
 }
 
@@ -757,7 +761,7 @@ function ajaxifyPost(postElement) {
        });
 
        /* add “comment” link. */
-       addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
+       addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
 
        /* process all replies. */
        replyIds = [];
@@ -817,7 +821,7 @@ function ajaxifyReply(replyElement) {
                        });
                });
        })(replyElement);
-       addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
+       addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
 
        /* convert “show source” link into javascript function. */
        $(replyElement).find(".show-reply-source").each(function() {
@@ -859,19 +863,19 @@ function ajaxifyNotification(notification) {
                notification.find(".text").addClass("hidden");
        }
        notification.find("form.mark-as-read button").click(function() {
-               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+               ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
        });
        notification.find("a[class^='link-']").each(function() {
                linkElement = $(this);
                if (linkElement.is("[href^='viewPost']")) {
                        id = linkElement.attr("class").substr(5);
                        if (hasPost(id)) {
-                               linkElement.attr("href", "#post-" + id);
+                               linkElement.attr("href", "#post-" + id).addClass("in-page-link");
                        }
                }
        });
        notification.find("form.dismiss button").click(function() {
-               $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+               ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
                        /* dismiss in case of error, too. */
                        notification.slideUp();
                }, function(xmlHttpRequest, textStatus, error) {
@@ -911,8 +915,8 @@ function checkForRemovedSones(oldNotification, newNotification) {
        if (getNotificationId(oldNotification) != "new-sone-notification") {
                return;
        }
-       oldIds = getElementIds(oldNotification, ".sone-id");
-       newIds = getElementIds(newNotification, ".sone-id");
+       oldIds = getElementIds(oldNotification, ".new-sone-id");
+       newIds = getElementIds(newNotification, ".new-sone-id");
        $.each(oldIds, function(index, value) {
                if ($.inArray(value, newIds) == -1) {
                        markSoneAsKnown(getSone(value), true);
@@ -966,11 +970,11 @@ function checkForRemovedReplies(oldNotification, newNotification) {
 }
 
 function getStatus() {
-       $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
+       ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
-                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
+                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated, value.lastUpdatedText);
                        });
                        /* search for removed notifications. */
                        $("#sone #notification-area .notification").each(function() {
@@ -984,7 +988,7 @@ function getStatus() {
                                });
                                if (!foundNotification) {
                                        if (notificationId == "new-sone-notification") {
-                                               $(".sone-id", this).each(function(index, element) {
+                                               $(".new-sone-id", this).each(function(index, element) {
                                                        soneId = $(this).text();
                                                        markSoneAsKnown(getSone(soneId), true);
                                                });
@@ -1009,25 +1013,16 @@ function getStatus() {
                                }
                        });
                        /* process notifications. */
+                       notificationIds = [];
                        $.each(data.notifications, function(index, value) {
                                oldNotification = getNotification(value.id);
-                               notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
-                               if (oldNotification.length != 0) {
-                                       if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
-                                               opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
-                                               notification.find(".short-text").toggleClass("hidden", opened);
-                                               notification.find(".text").toggleClass("hidden", !opened);
-                                       }
-                                       checkForRemovedSones(oldNotification, notification);
-                                       checkForRemovedPosts(oldNotification, notification);
-                                       checkForRemovedReplies(oldNotification, notification);
-                                       oldNotification.replaceWith(notification.show());
-                               } else {
-                                       $("#sone #notification-area").append(notification);
-                                       notification.slideDown();
-                                       setActivity();
+                               if ((oldNotification.length == 0) || (value.lastUpdatedTime > getNotificationLastUpdatedTime(oldNotification))) {
+                                       notificationIds.push(value.id);
                                }
                        });
+                       if (notificationIds.length > 0) {
+                               loadNotifications(notificationIds);
+                       }
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
                                loadNewPost(value.id, value.sone, value.recipient, value.time);
@@ -1042,10 +1037,44 @@ function getStatus() {
                        /* data.success was false, wait 30 seconds. */
                        setTimeout(getStatus, 30000);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* something really bad happend, wait a minute. */
-               setTimeout(getStatus, 60000);
-       })
+       }, function() {
+               statusRequestQueued = false;
+               ajaxError();
+       });
+}
+
+/**
+ * Requests multiple notifications from Sone and displays them.
+ *
+ * @param notificationIds
+ *            Array of IDs of the notifications to load
+ */
+function loadNotifications(notificationIds) {
+       ajaxGet("getNotification.ajax", {"notifications": notificationIds.join(",")}, function(data, textStatus) {
+               if (!data || !data.success) {
+                       // TODO - show error
+                       return;
+               }
+               $.each(data.notifications, function(index, value) {
+                       oldNotification = getNotification(value.id);
+                       notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+                       if (oldNotification.length != 0) {
+                               if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+                                       opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+                                       notification.find(".short-text").toggleClass("hidden", opened);
+                                       notification.find(".text").toggleClass("hidden", !opened);
+                               }
+                               checkForRemovedSones(oldNotification, notification);
+                               checkForRemovedPosts(oldNotification, notification);
+                               checkForRemovedReplies(oldNotification, notification);
+                               oldNotification.replaceWith(notification.show());
+                       } else {
+                               $("#sone #notification-area").append(notification);
+                               notification.slideDown();
+                               setActivity();
+                       }
+               });
+       });
 }
 
 /**
@@ -1078,6 +1107,22 @@ function isIndexPage() {
 }
 
 /**
+ * Returns the current page of the selected pagination. If no pagination can be
+ * found with the given selector, {@code 1} is returned.
+ *
+ * @param paginationSelector
+ *            The pagination selector
+ * @returns The current page of the pagination
+ */
+function getPage(paginationSelector) {
+       pagination = $(paginationSelector);
+       if (pagination.length > 0) {
+               return $(".current-page", paginationSelector).text();
+       }
+       return 1;
+}
+
+/**
  * Returns whether the current page is a “view Sone” page.
  *
  * @returns {Boolean} <code>true</code> if the current page is a “view Sone”
@@ -1155,7 +1200,7 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
-       if (!isIndexPage()) {
+       if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
                if (!isViewPostPage() || (getShownPostId() != postId)) {
                        if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
                                return;
@@ -1165,12 +1210,12 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (getPostTime($("#sone .post").last()) > time) {
                return;
        }
-       $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
+       ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
                                return;
                        }
-                       if (!isIndexPage() && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
+                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
                                return;
                        }
                        var firstOlderPost = null;
@@ -1199,7 +1244,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (!hasPost(postId)) {
                return;
        }
-       $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
                        if (hasReply(data.reply.id)) {
@@ -1243,11 +1288,10 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
  *            request
  */
 function markSoneAsKnown(soneElement, skipRequest) {
-       if ($(".new", soneElement).length > 0) {
-               if ((typeof skipRequest != "undefined") && !skipRequest) {
-                       $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
-                               $(soneElement).removeClass("new");
-                       });
+       if ($(soneElement).is(".new")) {
+               $(soneElement).removeClass("new");
+               if ((typeof skipRequest == "undefined") || !skipRequest) {
+                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)});
                }
        }
 }
@@ -1260,7 +1304,7 @@ function markPostAsKnown(postElements, skipRequest) {
                                $(postElement).removeClass("new");
                                $(".click-to-show", postElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
                                }
                        })(postElement);
                }
@@ -1275,7 +1319,7 @@ function markReplyAsKnown(replyElements, skipRequest) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
                                }
                        })(replyElement);
                }
@@ -1313,7 +1357,7 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updatePostTimes(postIds) {
-       $.getJSON("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.postTimes, function(index, value) {
                                updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1350,7 +1394,7 @@ function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updateReplyTimes(replyIds) {
-       $.getJSON("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.replyTimes, function(index, value) {
                                updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1439,10 +1483,10 @@ function changeIcon(iconUrl) {
  *            <code>true</code> if the notification can be dismissed by the
  *            user
  */
-function createNotification(id, text, dismissable) {
-       notification = $("<div></div>").addClass("notification").attr("id", id);
+function createNotification(id, lastUpdatedTime, text, dismissable) {
+       notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
        if (dismissable) {
-               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
+               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
                dismissForm.find("input[name=notification]").val(id);
                notification.append(dismissForm);
        }
@@ -1468,7 +1512,7 @@ function showNotificationDetails(notificationId) {
  *            The ID of the field to delete
  */
 function deleteProfileField(fieldId) {
-       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+       ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
                if (data && data.success) {
                        $("#sone .profile-field#" + data.field.id).slideUp();
                }
@@ -1486,7 +1530,7 @@ function deleteProfileField(fieldId) {
  *            Called when the renaming was successful
  */
 function editProfileField(fieldId, newName, successFunction) {
-       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+       ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1504,7 +1548,7 @@ function editProfileField(fieldId, newName, successFunction) {
  *            Function to call on success
  */
 function moveProfileField(fieldId, direction, successFunction) {
-       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+       ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1535,11 +1579,51 @@ function moveProfileFieldDown(fieldId, successFunction) {
        moveProfileField(fieldId, "down", successFunction);
 }
 
+var statusRequestQueued = true;
+
+/**
+ * Sets the status of the web interface as offline.
+ */
+function ajaxError() {
+       online = false;
+       toggleOfflineMarker(true);
+       if (!statusRequestQueued) {
+               setTimeout(getStatus, 5000);
+               statusRequestQueued = true;
+       }
+}
+
+/**
+ * Sets the status of the web interface as online.
+ */
+function ajaxSuccess() {
+       online = true;
+       toggleOfflineMarker(false);
+}
+
+/**
+ * Shows or hides the offline marker.
+ *
+ * @param visible
+ *            {@code true} to display the offline marker, {@code false} to hide
+ *            it
+ */
+function toggleOfflineMarker(visible) {
+       /* jQuery documentation says toggle() works the other way around?! */
+       $("#sone #offline-marker").toggle(visible);
+       if (visible) {
+               $("#sone #main").addClass("offline");
+       } else {
+               $("#sone #main").removeClass("offline");
+       }
+}
+
 //
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
 
 var focus = true;
+var online = true;
 
 $(document).ready(function() {
 
@@ -1561,7 +1645,7 @@ $(document).ready(function() {
                        }
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
                                button.removeAttr("disabled");
                        });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
@@ -1590,7 +1674,7 @@ $(document).ready(function() {
                $("#sone #post-message").submit(function() {
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
                        $(this).find(":input[name=text]:enabled").val("").blur();
                        $(this).find(".sender").hide();
@@ -1655,6 +1739,11 @@ $(document).ready(function() {
                ajaxifyNotification($(this));
        });
 
+       /* disable all permalinks. */
+       $(".permalink").click(function() {
+               return false;
+       });
+
        /* activate status polling. */
        setTimeout(getStatus, 5000);
 
@@ -1664,6 +1753,6 @@ $(document).ready(function() {
                resetActivity();
        }).blur(function() {
                focus = false;
-       })
+       });
 
 });
index fe9cf34..e80a92c 100644 (file)
@@ -1,4 +1,4 @@
-<div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
+<div id="sone">
 
        <div id="formPassword"><% formPassword|html></div>
        <div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
@@ -7,6 +7,8 @@
        <script src="javascript/jquery.url.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
 
+       <div id="offline-marker"></div>
+
        <div id="main">
 
                <div id="notification-area">
@@ -18,8 +20,8 @@
                                <button type="submit"><%= Notification.Button.Dismiss|l10n|html></button>
                        </form>
 
-                       <%foreach webInterface.notifications.all notification>
-                               <div class="notification" id="<% notification.id|html>">
+                       <%foreach notifications notification>
+                               <div class="notification" id="<% notification.id|html>" lastUpdatedTime="<%notification.lastUpdatedTime|html>">
                                        <%if notification.dismissable>
                                                <form class="dismiss" action="dismissNotification.html" method="post">
                                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
index a5efc97..f8e6f11 100644 (file)
@@ -1,5 +1,5 @@
 <%if pagination.necessary>
-       <div class="navigation">
+       <div class="navigation <%paginationName|html>">
                <div class="first"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter value=0>">«</a><%else><span>«</span><%/if></div>
                <div class="previous"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter key=pagination.previousPage>">‹</a><%else><span>‹</span><%/if></div>
                <div class="current-page"><% pagination.pageNumber></div>
index 4be1bcc..8b7ccb8 100644 (file)
                        </div>
                        <span class='separator'>·</span>
                        <div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-post"><a href="post://<%post.id|html>">[<%= View.Post.Permalink|l10n|html>]</a></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-author"><a href="sone://<%post.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
                        <%if ! originalText|match key=parsedText>
                                <span class='separator'>·</span>
                                <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
index 5dc26fb..2fa4885 100644 (file)
@@ -15,6 +15,8 @@
                </div>
                <div class="reply-status-line status-line">
                        <div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-author"><a href="sone://<%reply.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
                        <%if ! originalText|match key=parsedText>
                                <span class='separator'>·</span>
                                <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
index c403b0d..91c921d 100644 (file)
@@ -5,7 +5,7 @@
        <div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
        <div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
        <div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
-       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss"></span></div>
+       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
        <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
        <div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
        <div class="hidden"><% sone.blacklisted></div>
index 59bfee7..f79e52d 100644 (file)
@@ -8,7 +8,7 @@
 
        <div id="posts">
                <%= page|store key=pageParameter>
-               <%include include/pagination.html>
+               <%include include/pagination.html paginationName==pagination-index>
                <%foreach pagination.items post>
                        <%include include/viewPost.html>
                <%foreachelse>
index 73e35a8..7295361 100644 (file)
@@ -6,7 +6,7 @@
                <h1>Sone “<% currentSone.name|html>”</h1>
                <p>
                        This page should not be viewed directly; it is merely a reminder
-                       that you should install the <i>Sone</i> plugin.
+                       that you should install the <a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/<% currentEdition>/">Sone</a> plugin.
                </p>
        </body>
 </html>
index 54008dc..0cd71ff 100644 (file)
@@ -1,15 +1,15 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="post" />
+       <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewPost.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="post" />
-               <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%= Notification.NewPost.Text|l10n|html>
        <%foreach posts post>
                <div class="hidden post-id"><%post.id|html></div>
index ee8c410..a4588e5 100644 (file)
@@ -1,15 +1,15 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="reply" />
+       <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewReply.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="reply" />
-               <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
        <%= Notification.NewReply.Text|l10n|html>
        <%foreach replies postGroup|replyGroup>
index aaa0d14..64985d8 100644 (file)
@@ -1,18 +1,18 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="sone" />
+       <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewSone.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="sone" />
-               <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%= Notification.NewSone.Text|l10n|html>
        <%foreach sones sone>
-               <div class="hidden sone-id"><% sone.id|html></div>
+               <div class="hidden new-sone-id"><% sone.id|html></div>
                <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index dfeb3e9..368c14e 100644 (file)
@@ -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>
@@ -72,7 +91,9 @@
 
                <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>
index 066adef..689ff38 100644 (file)
@@ -7,7 +7,7 @@
 
                <h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
 
-               <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=sone.id|html></p>
+               <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=soneId|html></p>
 
        <%elseifnull sone.name>
 
@@ -25,7 +25,7 @@
 
                        <div class="profile-field">
                                <div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
-                               <div class="value"><a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+                               <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
                        </div>
 
                        <%foreach sone.profile.fields field>
@@ -76,7 +76,7 @@
 
                <%foreach repliedPosts post>
                        <%first>
-                               <h2><%= Page.ViewSone.Replies.Title|l10n|html></h2>
+                               <h2><%= Page.ViewSone.Replies.Title|l10n|html|replace needle="{sone}" replacementKey=sone.niceName></h2>
                                <div id="replied-posts">
                                        <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
                        <%/first>
index 7e05fd5..e667aeb 100644 (file)
@@ -41,7 +41,7 @@ public class FreenetLinkParserTest extends TestCase {
                TemplateContextFactory templateContextFactory = new TemplateContextFactory();
                templateContextFactory.addFilter("html", new HtmlFilter());
                FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
-               FreenetLinkParserContext context = new FreenetLinkParserContext(null);
+               FreenetLinkParserContext context = new FreenetLinkParserContext(null, null);
                Part part;
 
                part = parser.parse(context, new StringReader("Text."));