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

75 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/UpdateListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Fingerprintable.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/Parser.java
src/main/java/net/pterodactylus/sone/text/ParserContext.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/AboutPage.java
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/LikePage.java
src/main/java/net/pterodactylus/sone/web/LockSonePage.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/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/TrustPage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UnlikePage.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/ViewPostPage.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/DeleteProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/deleteProfileField.html [new file with mode: 0644]
src/main/resources/templates/editProfile.html
src/main/resources/templates/editProfileField.html [new file with mode: 0644]
src/main/resources/templates/include/head.html
src/main/resources/templates/include/tail.html
src/main/resources/templates/index.html
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html [new file with mode: 0644]
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/notify/newVersionNotification.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html

diff --git a/pom.xml b/pom.xml
index 1ceef4f..eae705c 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.3.6-4</version>
+       <version>0.3.7</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.7.5</version>
+                       <version>0.7.7</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 7277533..27e8db8 100644 (file)
@@ -34,6 +34,7 @@ import net.pterodactylus.sone.core.Options.OptionWatcher;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Identity;
@@ -48,6 +49,7 @@ import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.validation.Validation;
+import net.pterodactylus.util.version.Version;
 import freenet.keys.FreenetURI;
 
 /**
@@ -55,7 +57,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener {
+public class Core implements IdentityListener, UpdateListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -101,6 +103,9 @@ public class Core implements IdentityListener {
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** The update checker. */
+       private final UpdateChecker updateChecker;
+
        /** Whether the core has been stopped. */
        private volatile boolean stopped;
 
@@ -168,6 +173,7 @@ public class Core implements IdentityListener {
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
+               this.updateChecker = new UpdateChecker(freenetInterface);
        }
 
        //
@@ -239,6 +245,15 @@ public class Core implements IdentityListener {
        }
 
        /**
+        * Returns the update checker.
+        *
+        * @return The update checker
+        */
+       public UpdateChecker getUpdateChecker() {
+               return updateChecker;
+       }
+
+       /**
         * Returns the status of the given Sone.
         *
         * @param sone
@@ -1138,6 +1153,17 @@ public class Core implements IdentityListener {
                profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
                profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
 
+               /* load profile fields. */
+               while (true) {
+                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
+                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
+                       if (fieldName == null) {
+                               break;
+                       }
+                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
+                       profile.addField(fieldName).setValue(fieldValue);
+               }
+
                /* load posts. */
                Set<Post> posts = new HashSet<Post>();
                while (true) {
@@ -1271,6 +1297,15 @@ public class Core implements IdentityListener {
                        configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
                        configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
 
+                       /* save profile fields. */
+                       int fieldCounter = 0;
+                       for (Field profileField : profile.getFields()) {
+                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
                        /* save posts. */
                        int postCounter = 0;
                        for (Post post : sone.getPosts()) {
@@ -1522,6 +1557,8 @@ public class Core implements IdentityListener {
         */
        public void start() {
                loadConfiguration();
+               updateChecker.addUpdateListener(this);
+               updateChecker.start();
        }
 
        /**
@@ -1533,6 +1570,8 @@ public class Core implements IdentityListener {
                                soneInserter.stop();
                        }
                }
+               updateChecker.stop();
+               updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
                saveConfiguration();
                stopped = true;
@@ -1552,6 +1591,7 @@ public class Core implements IdentityListener {
 
                /* store the options first. */
                try {
+                       configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
@@ -1756,4 +1796,16 @@ public class Core implements IdentityListener {
                trustedIdentities.get(ownIdentity).remove(identity);
        }
 
+       //
+       // INTERFACE UpdateListener
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void updateFound(Version version, long releaseTime) {
+               coreListenerManager.fireUpdateFound(version, releaseTime);
+       }
+
 }
index 5d5e715..5fbb333 100644 (file)
@@ -22,6 +22,7 @@ import java.util.EventListener;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.version.Version;
 
 /**
  * Listener interface for objects that want to be notified on certain
@@ -127,4 +128,14 @@ public interface CoreListener extends EventListener {
         */
        public void soneUnlocked(Sone sone);
 
+       /**
+        * Notifies a listener that a new version has been found.
+        *
+        * @param version
+        *            The version that was found
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       public void updateFound(Version version, long releaseTime);
+
 }
index 464342c..4fc9532 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.event.AbstractListenerManager;
+import net.pterodactylus.util.version.Version;
 
 /**
  * Manager for {@link CoreListener}s.
@@ -197,4 +198,19 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
                }
        }
 
+       /**
+        * Notifies all listeners that a new version was found.
+        *
+        * @see CoreListener#updateFound(Version, long)
+        * @param version
+        *            The new version
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       void fireUpdateFound(Version version, long releaseTime) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.updateFound(version, releaseTime);
+               }
+       }
+
 }
index 6737d63..52c2857 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.core;
 
 import java.net.MalformedURLException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Level;
@@ -60,6 +61,9 @@ public class FreenetInterface {
        /** The USK callbacks. */
        private final Map<String, USKCallback> soneUskCallbacks = new HashMap<String, USKCallback>();
 
+       /** The not-Sone-related USK callbacks. */
+       private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
+
        /**
         * Creates a new Freenet interface.
         *
@@ -197,4 +201,83 @@ public class FreenetInterface {
                }
        }
 
+       /**
+        * Registers an arbitrary URI and calls the given callback if a new edition
+        * is found.
+        *
+        * @param uri
+        *            The URI to watch
+        * @param callback
+        *            The callback to call
+        */
+       public void registerUsk(FreenetURI uri, final Callback callback) {
+               USKCallback uskCallback = new USKCallback() {
+
+                       @Override
+                       public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
+                               callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo);
+                       }
+
+                       @Override
+                       public short getPollingPriorityNormal() {
+                               return RequestStarter.PREFETCH_PRIORITY_CLASS;
+                       }
+
+                       @Override
+                       public short getPollingPriorityProgress() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+
+               };
+               try {
+                       node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (HighLevelSimpleClientImpl) client);
+                       uriUskCallbacks.put(uri, uskCallback);
+               } catch (MalformedURLException mue1) {
+                       logger.log(Level.WARNING, "Could not subscribe to USK: " + uri, uri);
+               }
+       }
+
+       /**
+        * Unregisters the USK watcher for the given URI.
+        *
+        * @param uri
+        *            The URI to unregister the USK watcher for
+        */
+       public void unregisterUsk(FreenetURI uri) {
+               USKCallback uskCallback = uriUskCallbacks.remove(uri);
+               if (uskCallback == null) {
+                       logger.log(Level.INFO, "Could not unregister unknown USK: " + uri);
+                       return;
+               }
+               try {
+                       node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback);
+               } catch (MalformedURLException mue1) {
+                       logger.log(Level.INFO, "Could not unregister invalid USK: " + uri);
+               }
+       }
+
+       /**
+        * Callback for USK watcher events.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static interface Callback {
+
+               /**
+                * Notifies a listener that a new edition was found for a URI.
+                *
+                * @param uri
+                *            The URI that a new edition was found for
+                * @param edition
+                *            The found edition
+                * @param newKnownGood
+                *            Whether the found edition was actually fetched
+                * @param newSlot
+                *            Whether the found edition is higher than all previously
+                *            found editions
+                */
+               public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot);
+
+       }
+
 }
index 4328f02..af4155d 100644 (file)
@@ -310,6 +310,25 @@ public class SoneDownloader extends AbstractService {
                Profile profile = new Profile().setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
                profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
 
+               /* parse profile fields. */
+               SimpleXML profileFieldsXml = profileXml.getNode("fields");
+               if (profileFieldsXml != null) {
+                       for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
+                               String fieldName = fieldXml.getValue("field-name", null);
+                               String fieldValue = fieldXml.getValue("field-value", null);
+                               if ((fieldName == null) || (fieldValue == null)) {
+                                       logger.log(Level.WARNING, "Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", new Object[] { sone, fieldName, fieldValue });
+                                       return null;
+                               }
+                               try {
+                                       profile.addField(fieldName).setValue(fieldValue);
+                               } catch (IllegalArgumentException iae1) {
+                                       logger.log(Level.WARNING, "Duplicate field: " + fieldName, iae1);
+                                       return null;
+                               }
+                       }
+               }
+
                /* parse posts. */
                SimpleXML postsXml = soneXml.getNode("posts");
                Set<Post> posts = new HashSet<Post>();
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
new file mode 100644 (file)
index 0000000..0839cb9
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Sone - UpdateChecker.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.core;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.util.Date;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.util.collection.Pair;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.version.Version;
+import freenet.client.FetchResult;
+import freenet.keys.FreenetURI;
+import freenet.support.api.Bucket;
+
+/**
+ * Watches the official Sone homepage for new releases.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UpdateChecker {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(UpdateChecker.class);
+
+       /** The key of the Sone homepage. */
+       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 = 23;
+
+       /** The Freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The update listener manager. */
+       private final UpdateListenerManager updateListenerManager = new UpdateListenerManager();
+
+       /** The current URI of the homepage. */
+       private FreenetURI currentUri;
+
+       /** The current latest known version. */
+       private Version currentLatestVersion = SonePlugin.VERSION;
+
+       /** The release date of the latest version. */
+       private long latestVersionDate;
+
+       /**
+        * Creates a new update checker.
+        *
+        * @param freenetInterface
+        *            The freenet interface to use
+        */
+       public UpdateChecker(FreenetInterface freenetInterface) {
+               this.freenetInterface = freenetInterface;
+       }
+
+       //
+       // EVENT LISTENER MANAGEMENT
+       //
+
+       /**
+        * Adds the given listener to the list of registered listeners.
+        *
+        * @param updateListener
+        *            The listener to add
+        */
+       public void addUpdateListener(UpdateListener updateListener) {
+               updateListenerManager.addListener(updateListener);
+       }
+
+       /**
+        * Removes the given listener from the list of registered listeners.
+        *
+        * @param updateListener
+        *            The listener to remove
+        */
+       public void removeUpdateListener(UpdateListener updateListener) {
+               updateListenerManager.removeListener(updateListener);
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether a version that is later than the currently running
+        * version has been found.
+        *
+        * @return {@code true} if a new version was found
+        */
+       public boolean hasLatestVersion() {
+               return currentLatestVersion.compareTo(SonePlugin.VERSION) > 0;
+       }
+
+       /**
+        * Returns the latest version. If no new latest version has been found, the
+        * current version is returned.
+        *
+        * @return The latest known version
+        */
+       public Version getLatestVersion() {
+               return currentLatestVersion;
+       }
+
+       /**
+        * Returns the release time of the latest version. If no new latest version
+        * has been found, the returned value is undefined.
+        *
+        * @return The release time of the latest version, if a new version was
+        *         found
+        */
+       public long getLatestVersionDate() {
+               return latestVersionDate;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Starts the update checker.
+        */
+       public void start() {
+               try {
+                       currentUri = new FreenetURI(SONE_HOMEPAGE + LATEST_EDITION);
+               } catch (MalformedURLException mue1) {
+                       /* this can not really happen unless I screw up. */
+                       logger.log(Level.SEVERE, "Sone Homepage URI invalid!", mue1);
+               }
+               freenetInterface.registerUsk(currentUri, new FreenetInterface.Callback() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot) {
+                               logger.log(Level.FINEST, "Found update for %s: %d, %s, %s", new Object[] { uri, edition, newKnownGood, newSlot });
+                               if (newKnownGood || newSlot) {
+                                       Pair<FreenetURI, FetchResult> uriResult = freenetInterface.fetchUri(uri.setMetaString(new String[] { "sone.properties" }));
+                                       if (uriResult == null) {
+                                               logger.log(Level.WARNING, "Could not fetch properties of latest homepage: %s", uri);
+                                               return;
+                                       }
+                                       Bucket resultBucket = uriResult.getRight().asBucket();
+                                       try {
+                                               parseProperties(resultBucket.getInputStream());
+                                       } catch (IOException ioe1) {
+                                               logger.log(Level.WARNING, "Could not parse sone.properties of " + uri, ioe1);
+                                       } finally {
+                                               resultBucket.free();
+                                       }
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Stops the update checker.
+        */
+       public void stop() {
+               freenetInterface.unregisterUsk(currentUri);
+       }
+
+       //
+       // PRIVATE ACTIONS
+       //
+
+       /**
+        * Parses the properties of the latest version and fires events, if
+        * necessary.
+        *
+        * @see UpdateListener#updateFound(Version, long)
+        * @see UpdateListenerManager#fireUpdateFound(Version, long)
+        * @param propertiesInputStream
+        *            The input stream to parse
+        * @throws IOException
+        *             if an I/O error occured
+        */
+       private void parseProperties(InputStream propertiesInputStream) throws IOException {
+               Properties properties = new Properties();
+               InputStreamReader inputStreamReader = null;
+               try {
+                       inputStreamReader = new InputStreamReader(propertiesInputStream, "UTF-8");
+                       properties.load(inputStreamReader);
+               } finally {
+                       Closer.close(inputStreamReader);
+               }
+               String versionString = properties.getProperty("CurrentVersion/Version");
+               String releaseTimeString = properties.getProperty("CurrentVersion/ReleaseTime");
+               if ((versionString == null) || (releaseTimeString == null)) {
+                       logger.log(Level.INFO, "Invalid data parsed from properties.");
+                       return;
+               }
+               Version version = Version.parse(versionString);
+               long releaseTime = 0;
+               try {
+                       releaseTime = Long.parseLong(releaseTimeString);
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               if ((version == null) || (releaseTime == 0)) {
+                       logger.log(Level.INFO, "Could not parse data from properties.");
+                       return;
+               }
+               if (version.compareTo(currentLatestVersion) > 0) {
+                       currentLatestVersion = version;
+                       latestVersionDate = releaseTime;
+                       logger.log(Level.INFO, "Found new version: %s (%tc)", new Object[] { version, new Date(releaseTime) });
+                       updateListenerManager.fireUpdateFound(version, releaseTime);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListener.java b/src/main/java/net/pterodactylus/sone/core/UpdateListener.java
new file mode 100644 (file)
index 0000000..1469afa
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Sone - UpdateListener.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.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.util.version.Version;
+
+/**
+ * Listener interface for {@link UpdateChecker} events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface UpdateListener extends EventListener {
+
+       /**
+        * Notifies a listener that a newer version than the current version was
+        * found.
+        *
+        * @param version
+        *            The version that was found
+        * @param releaseTime
+        *            The release time of the version
+        */
+       public void updateFound(Version version, long releaseTime);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java b/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java
new file mode 100644 (file)
index 0000000..3f3b21f
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - UpdateListenerManager.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.core;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+import net.pterodactylus.util.version.Version;
+
+/**
+ * Listener manager for {@link UpdateListener} events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UpdateListenerManager extends AbstractListenerManager<Void, UpdateListener> {
+
+       /**
+        * Creates a new update listener manager.
+        */
+       public UpdateListenerManager() {
+               super(null);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Notifies all listeners that a new version has been found.
+        *
+        * @param version
+        *            The new version
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       void fireUpdateFound(Version version, long releaseTime) {
+               for (UpdateListener updateListener : getListeners()) {
+                       updateListener.updateFound(version, releaseTime);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/data/Fingerprintable.java b/src/main/java/net/pterodactylus/sone/data/Fingerprintable.java
new file mode 100644 (file)
index 0000000..013e0b3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sone - Fingerprintable.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.data;
+
+/**
+ * Interface for objects that can create a fingerprint of themselves, e.g. to
+ * detect modifications. The fingerprint should only contain original
+ * information; derived information should not be included.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Fingerprintable {
+
+       /**
+        * Returns the fingerprint of this object.
+        *
+        * @return The fingerprint of this object
+        */
+       public String getFingerprint();
+
+}
index 7c29430..8d4306d 100644 (file)
 
 package net.pterodactylus.sone.data;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
 /**
  * A profile stores personal information about a {@link Sone}. All information
  * is optional and can be {@code null}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Profile {
-
-       /** Whether the profile was modified. */
-       private volatile boolean modified;
+public class Profile implements Fingerprintable {
 
        /** The first name. */
        private volatile String firstName;
@@ -46,6 +50,9 @@ public class Profile {
        /** The year of the birth date. */
        private volatile Integer birthYear;
 
+       /** Additional fields in the profile. */
+       private final List<Field> fields = Collections.synchronizedList(new ArrayList<Field>());
+
        /**
         * Creates a new empty profile.
         */
@@ -69,6 +76,7 @@ public class Profile {
                this.birthDay = profile.birthDay;
                this.birthMonth = profile.birthMonth;
                this.birthYear = profile.birthYear;
+               this.fields.addAll(profile.fields);
        }
 
        //
@@ -76,18 +84,6 @@ public class Profile {
        //
 
        /**
-        * Returns whether this profile was modified after creation. To clear the
-        * “is modified” flag you need to create a new profile from this one using
-        * the {@link #Profile(Profile)} constructor.
-        *
-        * @return {@code true} if this profile was modified after creation,
-        *         {@code false} otherwise
-        */
-       public boolean isModified() {
-               return modified;
-       }
-
-       /**
         * Returns the first name.
         *
         * @return The first name
@@ -104,7 +100,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setFirstName(String firstName) {
-               modified |= ((firstName != null) && (!firstName.equals(this.firstName))) || (this.firstName != null);
                this.firstName = firstName;
                return this;
        }
@@ -126,7 +121,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setMiddleName(String middleName) {
-               modified |= ((middleName != null) && (!middleName.equals(this.middleName))) || (this.middleName != null);
                this.middleName = middleName;
                return this;
        }
@@ -148,7 +142,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setLastName(String lastName) {
-               modified |= ((lastName != null) && (!lastName.equals(this.lastName))) || (this.lastName != null);
                this.lastName = lastName;
                return this;
        }
@@ -170,7 +163,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthDay(Integer birthDay) {
-               modified |= ((birthDay != null) && (!birthDay.equals(this.birthDay))) || (this.birthDay != null);
                this.birthDay = birthDay;
                return this;
        }
@@ -192,7 +184,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthMonth(Integer birthMonth) {
-               modified |= ((birthMonth != null) && (!birthMonth.equals(this.birthMonth))) || (this.birthMonth != null);
                this.birthMonth = birthMonth;
                return this;
        }
@@ -214,9 +205,292 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthYear(Integer birthYear) {
-               modified |= ((birthYear != null) && (!birthYear.equals(this.birthYear))) || (this.birthYear != null);
                this.birthYear = birthYear;
                return this;
        }
 
+       /**
+        * Returns the fields of this profile.
+        *
+        * @return The fields of this profile
+        */
+       public List<Field> getFields() {
+               return new ArrayList<Field>(fields);
+       }
+
+       /**
+        * Returns whether this profile contains the given field.
+        *
+        * @param field
+        *            The field to check for
+        * @return {@code true} if this profile contains the field, false otherwise
+        */
+       public boolean hasField(Field field) {
+               return fields.contains(field);
+       }
+
+       /**
+        * Returns the field with the given ID.
+        *
+        * @param fieldId
+        *            The ID of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given ID
+        */
+       public Field getFieldById(String fieldId) {
+               Validation.begin().isNotNull("Field ID", fieldId).check();
+               for (Field field : fields) {
+                       if (field.getId().equals(fieldId)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Returns the field with the given name.
+        *
+        * @param fieldName
+        *            The name of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given name
+        */
+       public Field getFieldByName(String fieldName) {
+               for (Field field : fields) {
+                       if (field.getName().equals(fieldName)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Appends a new field to the list of fields.
+        *
+        * @param fieldName
+        *            The name of the new field
+        * @return The new field
+        * @throws IllegalArgumentException
+        *             if the name is not valid
+        */
+       public Field addField(String fieldName) throws IllegalArgumentException {
+               Validation.begin().isNotNull("Field Name", fieldName).check().isGreater("Field Name Length", fieldName.length(), 0).isNull("Field Name Unique", getFieldByName(fieldName)).check();
+               @SuppressWarnings("synthetic-access")
+               Field field = new Field().setName(fieldName);
+               fields.add(field);
+               return field;
+       }
+
+       /**
+        * Moves the given field up one position in the field list. The index of the
+        * field to move must be greater than {@code 0} (because you obviously can
+        * not move the first field further up).
+        *
+        * @param field
+        *            The field to move up
+        */
+       public void moveFieldUp(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isGreater("Field Index", getFieldIndex(field), 0).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex - 1, field);
+       }
+
+       /**
+        * Moves the given field down one position in the field list. The index of
+        * the field to move must be less than the index of the last field (because
+        * you obviously can not move the last field further down).
+        *
+        * @param field
+        *            The field to move down
+        */
+       public void moveFieldDown(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isLess("Field Index", getFieldIndex(field), fields.size() - 1).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex + 1, field);
+       }
+
+       /**
+        * Removes the given field.
+        *
+        * @param field
+        *            The field to remove
+        */
+       public void removeField(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).check();
+               fields.remove(field);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the index of the field with the given name.
+        *
+        * @param field
+        *            The name of the field
+        * @return The index of the field, or {@code -1} if there is no field with
+        *         the given name
+        */
+       private int getFieldIndex(Field field) {
+               return fields.indexOf(field);
+       }
+
+       //
+       // INTERFACE Fingerprintable
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Profile(");
+               if (firstName != null) {
+                       fingerprint.append("FirstName(").append(firstName).append(')');
+               }
+               if (middleName != null) {
+                       fingerprint.append("MiddleName(").append(middleName).append(')');
+               }
+               if (lastName != null) {
+                       fingerprint.append("LastName(").append(lastName).append(')');
+               }
+               if (birthDay != null) {
+                       fingerprint.append("BirthDay(").append(birthDay).append(')');
+               }
+               if (birthMonth != null) {
+                       fingerprint.append("BirthMonth(").append(birthMonth).append(')');
+               }
+               if (birthYear != null) {
+                       fingerprint.append("BirthYear(").append(birthYear).append(')');
+               }
+               fingerprint.append("ContactInformation(");
+               for (Field field : fields) {
+                       fingerprint.append(field.getName()).append('(').append(field.getValue()).append(')');
+               }
+               fingerprint.append(")");
+               fingerprint.append(")");
+
+               return fingerprint.toString();
+       }
+
+       /**
+        * Container for a profile field.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class Field {
+
+               /** The ID of the field. */
+               private final String id;
+
+               /** The name of the field. */
+               private String name;
+
+               /** The value of the field. */
+               private String value;
+
+               /**
+                * Creates a new field with a random ID.
+                */
+               private Field() {
+                       this(UUID.randomUUID().toString());
+               }
+
+               /**
+                * Creates a new field with the given ID.
+                *
+                * @param id
+                *            The ID of the field
+                */
+               private Field(String id) {
+                       Validation.begin().isNotNull("Field ID", id).check();
+                       this.id = id;
+               }
+
+               /**
+                * Returns the ID of this field.
+                *
+                * @return The ID of this field
+                */
+               public String getId() {
+                       return id;
+               }
+
+               /**
+                * Returns the name of this field.
+                *
+                * @return The name of this field
+                */
+               public String getName() {
+                       return name;
+               }
+
+               /**
+                * Sets the name of this field. The name must not be {@code null} and
+                * must not match any other fields in this profile but my match the name
+                * of this field.
+                *
+                * @param name
+                *            The new name of this field
+                * @return This field
+                */
+               public Field setName(String name) {
+                       Validation.begin().isNotNull("Field Name", name).check().is("Field Unique", (getFieldByName(name) == null) || equals(getFieldByName(name))).check();
+                       this.name = name;
+                       return this;
+               }
+
+               /**
+                * Returns the value of this field.
+                *
+                * @return The value of this field
+                */
+               public String getValue() {
+                       return value;
+               }
+
+               /**
+                * Sets the value of this field. While {@code null} is allowed, no
+                * guarantees are made that {@code null} values are correctly persisted
+                * across restarts of the plugin!
+                *
+                * @param value
+                *            The new value of this field
+                * @return This field
+                */
+               public Field setValue(String value) {
+                       this.value = value;
+                       return this;
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Field)) {
+                               return false;
+                       }
+                       Field field = (Field) object;
+                       return id.equals(field.id);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return id.hashCode();
+               }
+
+       }
+
 }
index 7d4f7ef..03dff75 100644 (file)
@@ -40,7 +40,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Sone {
+public class Sone implements Fingerprintable {
 
        /** comparator that sorts Sones by their nice name. */
        public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
@@ -580,36 +580,17 @@ public class Sone {
                return this;
        }
 
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
        /**
-        * Returns a fingerprint of this Sone. The fingerprint only depends on data
-        * that is actually stored when a Sone is inserted. The fingerprint can be
-        * used to detect changes in Sone data and can also be used to detect if
-        * previous changes are reverted.
-        *
-        * @return The fingerprint of this Sone
+        * {@inheritDoc}
         */
+       @Override
        public synchronized String getFingerprint() {
                StringBuilder fingerprint = new StringBuilder();
-               fingerprint.append("Profile(");
-               if (profile.getFirstName() != null) {
-                       fingerprint.append("FirstName(").append(profile.getFirstName()).append(')');
-               }
-               if (profile.getMiddleName() != null) {
-                       fingerprint.append("MiddleName(").append(profile.getMiddleName()).append(')');
-               }
-               if (profile.getLastName() != null) {
-                       fingerprint.append("LastName(").append(profile.getLastName()).append(')');
-               }
-               if (profile.getBirthDay() != null) {
-                       fingerprint.append("BirthDay(").append(profile.getBirthDay()).append(')');
-               }
-               if (profile.getBirthMonth() != null) {
-                       fingerprint.append("BirthMonth(").append(profile.getBirthMonth()).append(')');
-               }
-               if (profile.getBirthYear() != null) {
-                       fingerprint.append("BirthYear(").append(profile.getBirthYear()).append(')');
-               }
-               fingerprint.append(")");
+               fingerprint.append(profile.getFingerprint());
 
                fingerprint.append("Posts(");
                for (Post post : getPosts()) {
index 1bdab44..64f84e2 100644 (file)
@@ -78,7 +78,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 3, 6, 4);
+       public static final Version VERSION = new Version(0, 3, 7);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
@@ -166,28 +166,28 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
                        }
                }
 
-               /* create freenet interface. */
-               FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
+               boolean startupFailed = true;
+               try {
+                       /* create freenet interface. */
+                       FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
 
-               /* create web of trust connector. */
-               PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
-               WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
-               identityManager = new IdentityManager(webOfTrustConnector);
-               identityManager.setContext("Sone");
+                       /* create web of trust connector. */
+                       PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
+                       WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
+                       identityManager = new IdentityManager(webOfTrustConnector);
+                       identityManager.setContext("Sone");
 
-               /* create core. */
-               core = new Core(oldConfiguration, freenetInterface, identityManager);
+                       /* create core. */
+                       core = new Core(oldConfiguration, freenetInterface, identityManager);
 
-               /* create the web interface. */
-               webInterface = new WebInterface(this);
-               core.addCoreListener(webInterface);
+                       /* create the web interface. */
+                       webInterface = new WebInterface(this);
+                       core.addCoreListener(webInterface);
 
-               /* create the identity manager. */
-               identityManager.addIdentityListener(core);
+                       /* create the identity manager. */
+                       identityManager.addIdentityListener(core);
 
-               /* start core! */
-               boolean startupFailed = true;
-               try {
+                       /* start core! */
                        core.start();
                        if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) {
                                logger.log(Level.INFO, "Setting configuration to file-based configuration.");
diff --git a/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java b/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java
new file mode 100644 (file)
index 0000000..0cd55de
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sone - JavascriptFilter.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.template;
+
+import java.util.Map;
+
+import net.pterodactylus.util.number.Hex;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Filter;
+
+/**
+ * Escapes double quotes, backslashes, carriage returns and line feeds, and
+ * additionally encloses a given string with double quotes to make it possible
+ * to use a string in Javascript.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class JavascriptFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+               StringBuilder javascriptString = new StringBuilder();
+               javascriptString.append('"');
+               for (char c : String.valueOf(data).toCharArray()) {
+                       if (c == '\r') {
+                               javascriptString.append("\\r");
+                               continue;
+                       }
+                       if (c == '\n') {
+                               javascriptString.append("\\n");
+                               continue;
+                       }
+                       if (c == '\t') {
+                               javascriptString.append("\\t");
+                               continue;
+                       }
+                       if ((c == '"') || (c == '\\')) {
+                               javascriptString.append('\\');
+                               javascriptString.append(c);
+                       } else if (c < 32) {
+                               javascriptString.append("\\x").append(Hex.toHex((byte) c));
+                       } else {
+                               javascriptString.append(c);
+                       }
+               }
+               javascriptString.append('"');
+               return javascriptString.toString();
+       }
+
+}
index d5dd406..95dc614 100644 (file)
@@ -24,6 +24,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
+import net.pterodactylus.sone.text.FreenetLinkParserContext;
 import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.TemplateFactory;
@@ -79,10 +80,7 @@ public class PostAccessor extends ReflectionAccessor {
                                return null;
                        }
                        try {
-                               synchronized (linkParser) {
-                                       linkParser.setPostingSone(post.getSone());
-                                       return linkParser.parse(new StringReader(text));
-                               }
+                               return linkParser.parse(new FreenetLinkParserContext(post.getSone()), new StringReader(text));
                        } catch (IOException ioe1) {
                                /* ignore. */
                        }
index 715b87f..db6c528 100644 (file)
@@ -24,6 +24,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
+import net.pterodactylus.sone.text.FreenetLinkParserContext;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -72,10 +73,7 @@ public class ReplyAccessor extends ReflectionAccessor {
                } else if (member.equals("text")) {
                        String text = reply.getText();
                        try {
-                               synchronized (linkParser) {
-                                       linkParser.setPostingSone(reply.getSone());
-                                       return linkParser.parse(new StringReader(text));
-                               }
+                               return linkParser.parse(new FreenetLinkParserContext(reply.getSone()), new StringReader(text));
                        } catch (IOException ioe1) {
                                /* ignore. */
                        }
index b50113d..1ef77d2 100644 (file)
@@ -27,7 +27,6 @@ import java.util.logging.Logger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.TemplateFactory;
 import freenet.keys.FreenetURI;
@@ -37,7 +36,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FreenetLinkParser implements Parser {
+public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(FreenetLinkParser.class);
@@ -75,9 +74,6 @@ public class FreenetLinkParser implements Parser {
        /** The template factory. */
        private final TemplateFactory templateFactory;
 
-       /** The Sone that posted the currently parsed text. */
-       private Sone postingSone;
-
        /**
         * Creates a new freenet link parser.
         *
@@ -89,22 +85,6 @@ public class FreenetLinkParser implements Parser {
        }
 
        //
-       // ACCESSORS
-       //
-
-       /**
-        * Sets the Sone that posted the text that will be parsed in the next call
-        * to {@link #parse(Reader)}. You need to synchronize calling this method
-        * and {@link #parse(Reader)}!
-        *
-        * @param sone
-        *            The Sone that posted the text
-        */
-       public void setPostingSone(Sone sone) {
-               postingSone = sone;
-       }
-
-       //
        // PART METHODS
        //
 
@@ -112,7 +92,7 @@ public class FreenetLinkParser implements Parser {
         * {@inheritDoc}
         */
        @Override
-       public Part parse(Reader source) throws IOException {
+       public Part parse(FreenetLinkParserContext context, Reader source) throws IOException {
                PartContainer parts = new PartContainer();
                BufferedReader bufferedReader = (source instanceof BufferedReader) ? (BufferedReader) source : new BufferedReader(source);
                String line;
@@ -167,36 +147,35 @@ public class FreenetLinkParser implements Parser {
                                        String name = link;
                                        logger.log(Level.FINER, "Found link: %s", link);
                                        logger.log(Level.FINEST, "Next: %d, CHK: %d, SSK: %d, USK: %d", new Object[] { next, nextChk, nextSsk, nextUsk });
-                                       if (linkType == LinkType.KSK) {
-                                               name = link.substring(4);
-                                       } else if ((linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
-                                               if (name.indexOf('/') > -1) {
-                                                       if (!name.endsWith("/")) {
-                                                               name = name.substring(name.lastIndexOf('/') + 1);
-                                                       } else {
-                                                               if (name.indexOf('/') != name.lastIndexOf('/')) {
-                                                                       name = name.substring(name.lastIndexOf('/', name.lastIndexOf('/') - 1));
-                                                               } else {
-                                                                       /* shorten to 5 chars. */
-                                                                       name = name.substring(4, Math.min(9, name.length()));
-                                                               }
-                                                       }
-                                               }
+
+                                       if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                               FreenetURI uri;
                                                if (name.indexOf('?') > -1) {
                                                        name = name.substring(0, name.indexOf('?'));
                                                }
-                                               boolean fromPostingSone = false;
-                                               if ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
-                                                       try {
-                                                               new FreenetURI(link);
-                                                               fromPostingSone = link.substring(4, 47).equals(postingSone.getId());
-                                                               parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
-                                                       } catch (MalformedURLException mue1) {
-                                                               /* it’s not a valid link. */
-                                                               parts.add(createPlainTextPart(link));
+                                               if (name.endsWith("/")) {
+                                                       name = name.substring(0, name.length() - 1);
+                                               }
+                                               try {
+                                                       uri = new FreenetURI(name);
+                                                       name = uri.lastMetaString();
+                                                       if (name == null) {
+                                                               name = uri.getDocName();
+                                                       }
+                                                       if (name == null) {
+                                                               name = link.substring(0, Math.min(9, link.length()));
                                                        }
-                                               } else {
+                                                       boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
                                                        parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
+                                               } catch (MalformedURLException mue1) {
+                                                       /* not a valid link, insert as plain text. */
+                                                       parts.add(createPlainTextPart(link));
+                                               } catch (NullPointerException npe1) {
+                                                       /* FreenetURI sometimes throws these, too. */
+                                                       parts.add(createPlainTextPart(link));
+                                               } catch (ArrayIndexOutOfBoundsException aioobe1) {
+                                                       /* oh, and these, too. */
+                                                       parts.add(createPlainTextPart(link));
                                                }
                                        } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
                                                name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java
new file mode 100644 (file)
index 0000000..e524d91
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sone - FreenetLinkParserContext.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 net.pterodactylus.sone.data.Sone;
+
+/**
+ * {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
+ * stores the {@link Sone} that provided the parsed text so that certain links
+ * can be marked in a different way.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetLinkParserContext implements ParserContext {
+
+       /** The posting Sone. */
+       private final Sone postingSone;
+
+       /**
+        * Creates a new link parser context.
+        *
+        * @param postingSone
+        *            The posting Sone
+        */
+       public FreenetLinkParserContext(Sone postingSone) {
+               this.postingSone = postingSone;
+       }
+
+       /**
+        * Returns the Sone that provided the text that is being parsed.
+        *
+        * @return The posting Sone
+        */
+       public Sone getPostingSone() {
+               return postingSone;
+       }
+
+}
index ccec36a..e43ed47 100644 (file)
@@ -24,19 +24,23 @@ import java.io.Reader;
  * Interface for parsers that can create {@link Part}s from a text source
  * (usually a {@link Reader}).
  *
+ * @param <C>
+ *            The type of the parser context
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public interface Parser {
+public interface Parser<C extends ParserContext> {
 
        /**
         * Create a {@link Part} from the given text source.
         *
+        * @param context
+        *            The parser context
         * @param source
         *            The text source
         * @return The parsed part
         * @throws IOException
         *             if an I/O error occurs
         */
-       public Part parse(Reader source) throws IOException;
+       public Part parse(C context, Reader source) throws IOException;
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/text/ParserContext.java b/src/main/java/net/pterodactylus/sone/text/ParserContext.java
new file mode 100644 (file)
index 0000000..7f38ad8
--- /dev/null
@@ -0,0 +1,15 @@
+
+package net.pterodactylus.sone.text;
+
+/**
+ * Context for the {@link Parser}. This interface needs to be implemented by
+ * {@link Parser}s that need to provide more information than just the text to
+ * parse to {@link Parser#parse(ParserContext, java.io.Reader)}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ParserContext {
+
+       /* nothing to see. */
+
+}
index c20c72e..4a0dd22 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.version.Version;
 
@@ -53,9 +54,9 @@ public class AboutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
-               template.set("version", version);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               dataProvider.set("version", version);
        }
 
 }
index a8e2128..aba4700 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class CreatePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
                        String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
@@ -61,9 +62,9 @@ public class CreatePostPage extends SoneTemplatePage {
                                webInterface.getCore().createPost(currentSone, recipient, System.currentTimeMillis(), text);
                                throw new RedirectException(returnPage);
                        }
-                       template.set("errorTextEmpty", true);
+                       dataProvider.set("errorTextEmpty", true);
                }
-               template.set("returnPage", returnPage);
+               dataProvider.set("returnPage", returnPage);
        }
 
 }
index 2bfab6f..d4a9775 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class CreateReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
                String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
@@ -61,11 +62,11 @@ public class CreateReplyPage extends SoneTemplatePage {
                                webInterface.getCore().createReply(currentSone, post, text);
                                throw new RedirectException(returnPage);
                        }
-                       template.set("errorTextEmpty", true);
+                       dataProvider.set("errorTextEmpty", true);
                }
-               template.set("postId", postId);
-               template.set("text", text);
-               template.set("returnPage", returnPage);
+               dataProvider.set("postId", postId);
+               dataProvider.set("text", text);
+               dataProvider.set("returnPage", returnPage);
        }
 
 }
index e5f176f..46302b0 100644 (file)
@@ -30,6 +30,7 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.ToadletContext;
 
@@ -93,10 +94,10 @@ public class CreateSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
-               template.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
+               dataProvider.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("identity", 44);
                        OwnIdentity selectedIdentity = null;
@@ -107,7 +108,7 @@ public class CreateSonePage extends SoneTemplatePage {
                                }
                        }
                        if (selectedIdentity == null) {
-                               template.set("errorNoIdentity", true);
+                               dataProvider.set("errorNoIdentity", true);
                                return;
                        }
                        /* create Sone. */
index 4a64ad0..5722ee3 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,14 +50,14 @@ public class DeletePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.GET) {
                        String postId = request.getHttpRequest().getParam("post");
                        String returnPage = request.getHttpRequest().getParam("returnPage");
                        Post post = webInterface.getCore().getPost(postId);
-                       template.set("post", post);
-                       template.set("returnPage", returnPage);
+                       dataProvider.set("post", post);
+                       dataProvider.set("returnPage", returnPage);
                        return;
                } else if (request.getMethod() == Method.POST) {
                        String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
@@ -72,8 +73,8 @@ public class DeletePostPage extends SoneTemplatePage {
                        } else if (request.getHttpRequest().isPartSet("abortDelete")) {
                                throw new RedirectException(returnPage);
                        }
-                       template.set("post", post);
-                       template.set("returnPage", returnPage);
+                       dataProvider.set("post", post);
+                       dataProvider.set("returnPage", returnPage);
                }
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
new file mode 100644 (file)
index 0000000..9b52b04
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Sone - DeleteProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user confirm the deletion of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldPage(Template template, WebInterface webInterface) {
+               super("deleteProfileField.html", template, "Page.DeleteProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("confirm", 4).equals("true")) {
+                               fieldId = request.getHttpRequest().getParam("field");
+                               field = profile.getFieldById(fieldId);
+                               if (field == null) {
+                                       throw new RedirectException("invalid.html");
+                               }
+                               profile.removeField(field);
+                               currentSone.setProfile(profile);
+                       }
+                       throw new RedirectException("editProfile.html#profile-fields");
+               }
+
+               /* set current values in template. */
+               dataProvider.set("field", field);
+       }
+
+}
index 00a1a0e..8d90474 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class DeleteReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
                Reply reply = webInterface.getCore().getReply(replyId);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
@@ -66,8 +67,8 @@ public class DeleteReplyPage extends SoneTemplatePage {
                                throw new RedirectException(returnPage);
                        }
                }
-               template.set("reply", reply);
-               template.set("returnPage", returnPage);
+               dataProvider.set("reply", reply);
+               dataProvider.set("returnPage", returnPage);
        }
 
 }
index ae01573..abe26ea 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -50,8 +51,8 @@ public class DeleteSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        if (request.getHttpRequest().isPartSet("deleteSone")) {
                                Sone currentSone = getCurrentSone(request.getToadletContext());
index b1838da..2d61a95 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -47,8 +48,8 @@ public class DismissNotificationPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
                if ((notification != null) && notification.isDismissable()) {
index 021fa6b..9cbbb55 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -51,8 +52,8 @@ public class DistrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
                        String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
new file mode 100644 (file)
index 0000000..1b64b08
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sone - EditProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user edit the name of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldPage(Template template, WebInterface webInterface) {
+               super("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process the POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("cancel", 4).equals("true")) {
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       fieldId = request.getHttpRequest().getPartAsStringFailsafe("field", 36);
+                       field = profile.getFieldById(fieldId);
+                       if (field == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 256);
+                       Field existingField = profile.getFieldByName(name);
+                       if ((existingField == null) || (existingField.equals(field))) {
+                               field.setName(name);
+                               currentSone.setProfile(profile);
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       dataProvider.set("duplicateFieldName", true);
+               }
+
+               /* store current values in template. */
+               dataProvider.set("field", field);
+       }
+
+}
index 965caf9..380e054 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.util.List;
+
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.ToadletContext;
 
@@ -51,8 +55,8 @@ public class EditProfilePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                ToadletContext toadletContenxt = request.getToadletContext();
                Sone currentSone = getCurrentSone(toadletContenxt);
                Profile profile = currentSone.getProfile();
@@ -62,28 +66,99 @@ public class EditProfilePage extends SoneTemplatePage {
                Integer birthDay = profile.getBirthDay();
                Integer birthMonth = profile.getBirthMonth();
                Integer birthYear = profile.getBirthYear();
+               List<Field> fields = profile.getFields();
                if (request.getMethod() == Method.POST) {
-                       firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
-                       middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
-                       lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
-                       birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
-                       birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
-                       birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
-                       profile.setFirstName(firstName.length() > 0 ? firstName : null);
-                       profile.setMiddleName(middleName.length() > 0 ? middleName : null);
-                       profile.setLastName(lastName.length() > 0 ? lastName : null);
-                       profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
-                       if (profile.isModified()) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("save-profile", 4).equals("true")) {
+                               firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
+                               middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
+                               lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
+                               birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
+                               birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
+                               birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
+                               profile.setFirstName(firstName.length() > 0 ? firstName : null);
+                               profile.setMiddleName(middleName.length() > 0 ? middleName : null);
+                               profile.setLastName(lastName.length() > 0 ? lastName : null);
+                               profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
+                               for (Field field : fields) {
+                                       String value = request.getHttpRequest().getPartAsStringFailsafe("field-" + field.getId(), 400);
+                                       field.setValue(value);
+                               }
                                currentSone.setProfile(profile);
+                               webInterface.getCore().saveSone(currentSone);
+                               throw new RedirectException("editProfile.html");
+                       } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
+                               String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
+                               try {
+                                       profile.addField(fieldName);
+                                       currentSone.setProfile(profile);
+                                       fields = profile.getFields();
+                                       webInterface.getCore().saveSone(currentSone);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               } catch (IllegalArgumentException iae1) {
+                                       dataProvider.set("fieldName", fieldName);
+                                       dataProvider.set("duplicateFieldName", true);
+                               }
+                       } else {
+                               String id = getFieldId(request, "delete-field-");
+                               if (id != null) {
+                                       throw new RedirectException("deleteProfileField.html?field=" + id);
+                               }
+                               id = getFieldId(request, "move-up-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldUp(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "move-down-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldDown(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "edit-field-");
+                               if (id != null) {
+                                       throw new RedirectException("editProfileField.html?field=" + id);
+                               }
                        }
-                       throw new RedirectException("index.html");
                }
-               template.set("firstName", firstName);
-               template.set("middleName", middleName);
-               template.set("lastName", lastName);
-               template.set("birthDay", birthDay);
-               template.set("birthMonth", birthMonth);
-               template.set("birthYear", birthYear);
+               dataProvider.set("firstName", firstName);
+               dataProvider.set("middleName", middleName);
+               dataProvider.set("lastName", lastName);
+               dataProvider.set("birthDay", birthDay);
+               dataProvider.set("birthMonth", birthMonth);
+               dataProvider.set("birthYear", birthYear);
+               dataProvider.set("fields", fields);
        }
 
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Searches for a part whose names starts with the given {@code String} and
+        * extracts the ID from the located name.
+        *
+        * @param request
+        *            The request to get the parts from
+        * @param partNameStart
+        *            The start of the name of the requested part
+        * @return The parsed ID, or {@code null} if there was no part matching the
+        *         given string
+        */
+       private String getFieldId(Request request, String partNameStart) {
+               for (String partName : request.getHttpRequest().getParts()) {
+                       if (partName.startsWith(partNameStart)) {
+                               return partName.substring(partNameStart.length());
+                       }
+               }
+               return null;
+       }
 }
index c71c280..864480a 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -46,8 +47,8 @@ public class FollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index a33072f..b0a27f7 100644 (file)
@@ -24,6 +24,9 @@ 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.util.collection.Pagination;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -52,8 +55,8 @@ public class IndexPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                List<Post> allPosts = new ArrayList<Post>();
                allPosts.addAll(currentSone.getPosts());
@@ -71,16 +74,18 @@ public class IndexPage extends SoneTemplatePage {
                        }
                }
                Collections.sort(allPosts, Post.TIME_COMPARATOR);
-               template.set("posts", allPosts);
+               Pagination<Post> pagination = new Pagination<Post>(allPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               dataProvider.set("pagination", pagination);
+               dataProvider.set("posts", pagination.getItems());
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, Template template) {
+       protected void postProcess(Request request, DataProvider dataProvider) {
                @SuppressWarnings("unchecked")
-               List<Post> posts = (List<Post>) template.get("posts");
+               List<Post> posts = (List<Post>) dataProvider.get("posts");
                for (Post post : posts) {
                        webInterface.getCore().markPostKnown(post);
                        for (Reply reply : webInterface.getCore().getReplies(post)) {
index d413f61..774f410 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -51,11 +52,11 @@ public class KnownSonesPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                List<Sone> knownSones = new ArrayList<Sone>(webInterface.getCore().getSones());
                Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
-               template.set("knownSones", knownSones);
+               dataProvider.set("knownSones", knownSones);
        }
 
 }
index dd693ef..c84ea57 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class LikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
index 05c8ad7..7888659 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -48,8 +49,8 @@ public class LockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone != null) {
index dcfd8a7..ff81562 100644 (file)
@@ -26,6 +26,7 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.ToadletContext;
 
@@ -60,12 +61,12 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                /* get all own identities. */
                List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
                Collections.sort(localSones, Sone.NICE_NAME_COMPARATOR);
-               template.set("sones", localSones);
+               dataProvider.set("sones", localSones);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone-id", 100);
                        Sone selectedSone = webInterface.getCore().getLocalSone(soneId, false);
@@ -75,7 +76,7 @@ public class LoginPage extends SoneTemplatePage {
                        }
                }
                List<OwnIdentity> ownIdentitiesWithoutSone = CreateSonePage.getOwnIdentitiesWithoutSone(webInterface.getCore());
-               template.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
+               dataProvider.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
        }
 
        /**
index f1c0b48..0b6e6e7 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.ToadletContext;
 
@@ -45,9 +46,9 @@ public class LogoutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
                setCurrentSone(request.getToadletContext(), null);
-               super.processTemplate(request, template);
+               super.processTemplate(request, dataProvider);
                throw new RedirectException("index.html");
        }
 
index 35f54a1..0739ae7 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.core.Options;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class OptionsPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                Options options = webInterface.getCore().getOptions();
                if (request.getMethod() == Method.POST) {
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
@@ -73,13 +74,13 @@ public class OptionsPage extends SoneTemplatePage {
                        webInterface.getCore().saveConfiguration();
                        throw new RedirectException(getPath());
                }
-               template.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
-               template.set("positive-trust", options.getIntegerOption("PositiveTrust").get());
-               template.set("negative-trust", options.getIntegerOption("NegativeTrust").get());
-               template.set("trust-comment", options.getStringOption("TrustComment").get());
-               template.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
-               template.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
-               template.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
+               dataProvider.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
+               dataProvider.set("positive-trust", options.getIntegerOption("PositiveTrust").get());
+               dataProvider.set("negative-trust", options.getIntegerOption("NegativeTrust").get());
+               dataProvider.set("trust-comment", options.getStringOption("TrustComment").get());
+               dataProvider.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
+               dataProvider.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
+               dataProvider.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
        }
 
 }
index 23ca247..dc41865 100644 (file)
@@ -21,8 +21,10 @@ import java.util.Arrays;
 import java.util.Collection;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.web.page.Page;
 import net.pterodactylus.sone.web.page.TemplatePage;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
@@ -184,10 +186,14 @@ public class SoneTemplatePage extends TemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
-               template.set("currentSone", getCurrentSone(request.getToadletContext(), false));
-               template.set("request", request);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               dataProvider.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+               dataProvider.set("request", request);
+               dataProvider.set("currentVersion", SonePlugin.VERSION);
+               dataProvider.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+               dataProvider.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+               dataProvider.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
        }
 
        /**
index ef64d62..85341ad 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -51,8 +52,8 @@ public class TrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
                        String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
index 5b0c652..316e408 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -46,8 +47,8 @@ public class UnfollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index 1ecacc8..d08689f 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -49,8 +50,8 @@ public class UnlikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
index 938e451..35d69ff 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -47,8 +48,8 @@ public class UnlockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone != null) {
index 8126215..cc9e26d 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -51,8 +52,8 @@ public class UntrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                if (request.getMethod() == Method.POST) {
                        String returnPath = request.getHttpRequest().getPartAsStringFailsafe("returnPath", 256);
                        String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
index 3bc3b36..3c4eddf 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -48,19 +49,19 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId);
-               template.set("post", post);
+               dataProvider.set("post", post);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, Template template) {
-               Post post = (Post) template.get("post");
+       protected void postProcess(Request request, DataProvider dataProvider) {
+               Post post = (Post) dataProvider.get("post");
                webInterface.getCore().markPostKnown(post);
                for (Reply reply : webInterface.getCore().getReplies(post)) {
                        webInterface.getCore().markReplyKnown(reply);
index d933bc7..9fd6fae 100644 (file)
@@ -22,6 +22,7 @@ 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.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 
 /**
@@ -51,19 +52,19 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, Template template) throws RedirectException {
-               super.processTemplate(request, template);
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
-               template.set("sone", sone);
+               dataProvider.set("sone", sone);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, Template template) {
-               Sone sone = (Sone) template.get("sone");
+       protected void postProcess(Request request, DataProvider dataProvider) {
+               Sone sone = (Sone) dataProvider.get("sone");
                List<Post> posts = sone.getPosts();
                for (Post post : posts) {
                        webInterface.getCore().markPostKnown(post);
index 0c6322a..adcc17e 100644 (file)
@@ -48,6 +48,7 @@ import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.GetPagePlugin;
 import net.pterodactylus.sone.template.IdentityAccessor;
+import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
@@ -58,9 +59,11 @@ import net.pterodactylus.sone.template.TrustAccessor;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
+import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
@@ -71,6 +74,7 @@ import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.MarkPostAsKnownPage;
 import net.pterodactylus.sone.web.ajax.MarkReplyAsKnownPage;
+import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
@@ -94,6 +98,7 @@ import net.pterodactylus.util.template.TemplateFactory;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.version.Version;
 import freenet.clients.http.SessionManager;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
@@ -147,6 +152,9 @@ public class WebInterface implements CoreListener {
        /** The “Sone locked” notification. */
        private final ListNotification<Sone> lockedSonesNotification;
 
+       /** The “new version” notification. */
+       private final TemplateNotification newVersionNotification;
+
        /**
         * Creates a new web interface.
         *
@@ -173,6 +181,7 @@ public class WebInterface implements CoreListener {
                templateFactory.addFilter("change", new RequestChangeFilter());
                templateFactory.addFilter("match", new MatchFilter());
                templateFactory.addFilter("css", new CssClassNameFilter());
+               templateFactory.addFilter("js", new JavascriptFilter());
                templateFactory.addPlugin("getpage", new GetPagePlugin());
                templateFactory.addPlugin("paginate", new PaginationPlugin());
                templateFactory.setTemplateProvider(new ClassPathTemplateProvider(templateFactory));
@@ -196,6 +205,9 @@ public class WebInterface implements CoreListener {
 
                Template lockedSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
+
+               Template newVersionTemplate = templateFactory.createTemplate(createReader("/templates/notify/newVersionNotification.html"));
+               newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
        }
 
        //
@@ -474,6 +486,8 @@ public class WebInterface implements CoreListener {
                Template createPostTemplate = templateFactory.createTemplate(createReader("/templates/createPost.html"));
                Template createReplyTemplate = templateFactory.createTemplate(createReader("/templates/createReply.html"));
                Template editProfileTemplate = templateFactory.createTemplate(createReader("/templates/editProfile.html"));
+               Template editProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/editProfileField.html"));
+               Template deleteProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/deleteProfileField.html"));
                Template viewSoneTemplate = templateFactory.createTemplate(createReader("/templates/viewSone.html"));
                Template viewPostTemplate = templateFactory.createTemplate(createReader("/templates/viewPost.html"));
                Template deletePostTemplate = templateFactory.createTemplate(createReader("/templates/deletePost.html"));
@@ -482,6 +496,7 @@ public class WebInterface implements CoreListener {
                Template noPermissionTemplate = templateFactory.createTemplate(createReader("/templates/noPermission.html"));
                Template optionsTemplate = templateFactory.createTemplate(createReader("/templates/options.html"));
                Template aboutTemplate = templateFactory.createTemplate(createReader("/templates/about.html"));
+               Template invalidTemplate = templateFactory.createTemplate(createReader("/templates/invalid.html"));
                Template postTemplate = templateFactory.createTemplate(createReader("/templates/include/viewPost.html"));
                Template replyTemplate = templateFactory.createTemplate(createReader("/templates/include/viewReply.html"));
 
@@ -490,6 +505,8 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfilePage(editProfileTemplate, this), "EditProfile"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldPage(editProfileFieldTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostPage(createPostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyPage(createReplyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewSonePage(viewSoneTemplate, this)));
@@ -512,6 +529,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
@@ -536,6 +554,9 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
 
                ToadletContainer toadletContainer = sonePlugin.pluginRespirator().getToadletContainer();
                toadletContainer.getPageMaker().addNavigationCategory("/Sone/index.html", "Navigation.Menu.Name", "Navigation.Menu.Tooltip", sonePlugin);
@@ -707,6 +728,16 @@ public class WebInterface implements CoreListener {
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void updateFound(Version version, long releaseTime) {
+               newVersionNotification.set("version", version);
+               newVersionNotification.set("releaseTime", releaseTime);
+               notificationManager.addNotification(newVersionNotification);
+       }
+
+       /**
         * Template provider implementation that uses
         * {@link WebInterface#createReader(String)} to load templates for
         * inclusion.
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..383d2d4
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sone - DeleteProfileFieldAjaxPage.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.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user delete a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “delete profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldAjaxPage(WebInterface webInterface) {
+               super("deleteProfileField.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               profile.removeField(field);
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
+       }
+
+}
index 283e924..08e3ee5 100644 (file)
@@ -55,4 +55,12 @@ public class DismissNotificationAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..0036545
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - EditProfileFieldAjaxPage.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.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user rename a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “edit profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldAjaxPage(WebInterface webInterface) {
+               super("editProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String name = request.getHttpRequest().getParam("name", "").trim();
+               if (name.length() == 0) {
+                       return createErrorJsonObject("invalid-parameter-name");
+               }
+               Field existingField = profile.getFieldByName(name);
+               if ((existingField != null) && !existingField.equals(field)) {
+                       return createErrorJsonObject("duplicate-field-name");
+               }
+               field.setName(name);
+               currentSone.setProfile(profile);
+               return createSuccessJsonObject();
+       }
+
+}
index 015add5..659c8d2 100644 (file)
@@ -91,13 +91,23 @@ public class GetStatusAjaxPage extends JsonPage {
                Set<Post> newPosts = webInterface.getNewPosts();
                JsonArray jsonPosts = new JsonArray();
                for (Post post : newPosts) {
-                       jsonPosts.add(post.getId());
+                       JsonObject jsonPost = new JsonObject();
+                       jsonPost.put("id", post.getId());
+                       jsonPost.put("sone", post.getSone().getId());
+                       jsonPost.put("recipient", (post.getRecipient() != null) ? post.getRecipient().getId() : null);
+                       jsonPost.put("time", post.getTime());
+                       jsonPosts.add(jsonPost);
                }
                /* load new replies. */
                Set<Reply> newReplies = webInterface.getNewReplies();
                JsonArray jsonReplies = new JsonArray();
                for (Reply reply : newReplies) {
-                       jsonReplies.add(reply.getId());
+                       JsonObject jsonReply = new JsonObject();
+                       jsonReply.put("id", reply.getId());
+                       jsonReply.put("sone", reply.getSone().getId());
+                       jsonReply.put("post", reply.getPost().getId());
+                       jsonReply.put("postSone", reply.getPost().getSone().getId());
+                       jsonReplies.add(jsonReply);
                }
                return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("removedNotifications", jsonRemovedNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
@@ -110,6 +120,14 @@ public class GetStatusAjaxPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
        //
        // PRIVATE METHODS
        //
index 36327ca..725b13a 100644 (file)
@@ -59,4 +59,12 @@ public class GetTranslationPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
index 605afaf..8d48bce 100644 (file)
@@ -137,6 +137,16 @@ public abstract class JsonPage implements Page {
                return true;
        }
 
+       /**
+        * Returns whether this page requires the user to be logged in.
+        *
+        * @return {@code true} if the user needs to be logged in to use this page,
+        *         {@code false} otherwise
+        */
+       protected boolean requiresLogin() {
+               return true;
+       }
+
        //
        // PROTECTED METHODS
        //
@@ -184,6 +194,11 @@ public abstract class JsonPage implements Page {
                                return new Response(401, "Not authorized", "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")));
+                       }
+               }
                JsonObject jsonObject = createJsonObject(request);
                return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
        }
index 6c4ece0..bca9a35 100644 (file)
@@ -53,4 +53,12 @@ public class LockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..780e4d1
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - MoveProfileFieldAjaxPage.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.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user move a profile field up or down.
+ *
+ * @see Profile#moveFieldUp(Field)
+ * @see Profile#moveFieldDown(Field)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MoveProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “move profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MoveProfileFieldAjaxPage(WebInterface webInterface) {
+               super("moveProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String direction = request.getHttpRequest().getParam("direction");
+               try {
+                       if ("up".equals(direction)) {
+                               profile.moveFieldUp(field);
+                       } else if ("down".equals(direction)) {
+                               profile.moveFieldDown(field);
+                       } else {
+                               return createErrorJsonObject("invalid-direction");
+                       }
+               } catch (IllegalArgumentException iae1) {
+                       return createErrorJsonObject("not-possible");
+               }
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject();
+       }
+
+}
index 02682b2..e1c408c 100644 (file)
@@ -53,4 +53,12 @@ public class UnlockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
index e823eb5..e6ee539 100644 (file)
@@ -25,6 +25,7 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.PageMaker;
@@ -46,7 +47,7 @@ public class TemplatePage implements Page, LinkEnabledCallback {
        private final String path;
 
        /** The template to render. */
-       protected final Template template;
+       private final Template template;
 
        /** The L10n handler. */
        private final BaseL10n l10n;
@@ -116,9 +117,10 @@ public class TemplatePage implements Page, LinkEnabledCallback {
                        pageNode.addForwardLink("icon", shortcutIcon);
                }
 
+               DataProvider dataProvider = template.createDataProvider();
                try {
                        long start = System.nanoTime();
-                       processTemplate(request, template);
+                       processTemplate(request, dataProvider);
                        long finish = System.nanoTime();
                        logger.log(Level.FINEST, "Template was rendered in " + ((finish - start) / 1000) / 1000.0 + "ms.");
                } catch (RedirectException re1) {
@@ -126,10 +128,10 @@ public class TemplatePage implements Page, LinkEnabledCallback {
                }
 
                StringWriter stringWriter = new StringWriter();
-               template.render(stringWriter);
+               template.render(dataProvider, stringWriter);
                pageNode.content.addChild("%", stringWriter.toString());
 
-               postProcess(request, template);
+               postProcess(request, dataProvider);
 
                return new Response(200, "OK", "text/html", pageNode.outer.generate());
        }
@@ -159,29 +161,29 @@ public class TemplatePage implements Page, LinkEnabledCallback {
         *
         * @param request
         *            The request that is rendered
-        * @param template
-        *            The template to set variables in
+        * @param dataProvider
+        *            The data provider to set variables in
         * @throws RedirectException
         *             if the processing page wants to redirect after processing
         */
-       protected void processTemplate(Request request, Template template) throws RedirectException {
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
                /* do nothing. */
        }
 
        /**
         * This method will be called after
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, Template)}
+        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
         * has processed the template and the template was rendered. This method
         * will not be called if
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, Template)}
+        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
         * throws a {@link RedirectException}!
         *
         * @param request
         *            The request being processed
-        * @param template
-        *            The template that was rendered
+        * @param dataProvider
+        *            The data provider that supplied the rendered data
         */
-       protected void postProcess(Request request, Template template) {
+       protected void postProcess(Request request, DataProvider dataProvider) {
                /* do nothing. */
        }
 
@@ -212,7 +214,7 @@ public class TemplatePage implements Page, LinkEnabledCallback {
        /**
         * Exception that can be thrown to signal that a subclassed {@link Page}
         * wants to redirect the user during the
-        * {@link TemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, Template)}
+        * {@link TemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
         * method call.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
index d5d436b..76d8941 100644 (file)
@@ -74,8 +74,32 @@ Page.EditProfile.Birthday.Title=Birthday
 Page.EditProfile.Birthday.Label.Day=Day:
 Page.EditProfile.Birthday.Label.Month=Month:
 Page.EditProfile.Birthday.Label.Year=Year:
-Page.EditProfile.Page.Status.Changed=Your changes have been saved and will be inserted shortly.
+Page.EditProfile.Fields.Title=Custom Fields
+Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more.
+Page.EditProfile.Fields.Button.Edit=edit
+Page.EditProfile.Fields.Button.MoveUp=move up
+Page.EditProfile.Fields.Button.MoveDown=move down
+Page.EditProfile.Fields.Button.Delete=delete
+Page.EditProfile.Fields.Button.ReallyDelete=really delete
+Page.EditProfile.Fields.AddField.Title=Add Field
+Page.EditProfile.Fields.AddField.Label.Name=Name:
+Page.EditProfile.Fields.AddField.Button.AddField=Add Field
 Page.EditProfile.Button.Save=Save Profile
+Page.EditProfile.Error.DuplicateFieldName=The field name “{fieldName}” does already exist.
+
+Page.EditProfileField.Title=Edit Profile Field - Sone
+Page.EditProfileField.Page.Title=Edit Profile Field
+Page.EditProfileField.Text=Enter a new name for this profile field.
+Page.EditProfileField.Error.DuplicateFieldName=The field name you entered does already exist.
+Page.EditProfileField.Button.Save=Change
+Page.EditProfileField.Button.Reset=Revert to old name
+Page.EditProfileField.Button.Cancel=Do not change name
+
+Page.DeleteProfileField.Title=Delete Profile Field - Sone
+Page.DeleteProfileField.Page.Title=Delete Profile Field
+Page.DeleteProfileField.Text=Do you really want to delete this profile field?
+Page.DeleteProfileField.Button.Yes=Yes, delete
+Page.DeleteProfileField.Button.No=No, do not delete
 
 Page.CreatePost.Title=Create Post - Sone
 Page.CreatePost.Page.Title=Create Post
@@ -96,6 +120,8 @@ Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Plea
 Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
 Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
+Page.ViewSone.Profile.Title=Profile
+Page.ViewSone.Profile.Label.Name=Name
 
 Page.ViewPost.Title=View Post - Sone
 Page.ViewPost.Page.Title=View Post by {sone}
@@ -142,6 +168,10 @@ Page.WotPluginMissing.Text.LoadPlugin=Please load the Web of Trust plugin in the
 
 Page.Logout.Title=Logout - Sone
 
+Page.Invalid.Title=Invalid Action Performed
+Page.Invalid.Page.Title=Invalid Action Performed
+Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
+
 View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
 View.CreateSone.Select.Default=Select an identity
 View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
@@ -182,6 +212,7 @@ WebInterface.DefaultText.LastName=Last name
 WebInterface.DefaultText.BirthDay=Day
 WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
+WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
@@ -189,6 +220,8 @@ WebInterface.SelectBox.Choose=Choose…
 WebInterface.SelectBox.Yes=Yes
 WebInterface.SelectBox.No=No
 WebInterface.ClickToShow.Replies=Click here to show hidden replies.
+WebInterface.VersionInformation.CurrentVersion=Current Version:
+WebInterface.VersionInformation.LatestVersion=Latest Version:
 
 Notification.ClickHereToRead=Click here to read the full text of the notification.
 Notification.FirstStart.Text=This seems to be the first time you start Sone. To start, create a new Sone from a web of trust identity and start following other Sones.
@@ -205,3 +238,4 @@ Notification.SoneIsBeingRescued.Text=The following Sones are currently being res
 Notification.SoneRescued.Text=The following Sones have been rescued:
 Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
 Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
+Notification.NewVersion.Text=A new version of the Sone plugin was found: Version {version}.
index d1f85a2..54cfb6e 100644 (file)
@@ -16,10 +16,21 @@ input[type=text], textarea {
        outline: none;
 }
 
+input[type=text].short {
+       width: 25em;
+}
+
 textarea {
        height: 4em;
 }
 
+#sone button {
+       background-color: #ddd;
+       border-width: 1px;
+       color: #444;
+       padding: 0.5ex 1.5ex;
+}
+
 #sone form {
        margin: 0px;
 }
@@ -49,6 +60,10 @@ textarea {
        color: rgb(255, 172, 0);
 }
 
+#sone a.link {
+       cursor: pointer;
+}
+
 #sone a.internet {
        color: rgb(255, 0, 0);
 }
@@ -438,8 +453,40 @@ textarea {
        display: inline;
 }
 
-#sone #create-sone {
+#sone .profile-field, #sone #edit-profile button[type=submit], #sone #delete-profile-field {
+       margin-top: 1em;
+}
 
+#sone .profile-field .name {
+       display: inline;
+       font-weight: bold;
+}
+
+#sone .profile-field .name.hidden {
+       display: none;
+}
+
+#sone .profile-field button.confirm {
+       font-weight: bold;
+       color: #080;
+}
+
+#sone .profile-field button.cancel {
+       font-weight: bold;
+       color: red;
+}
+
+#sone .profile-field .value {
+       margin-left: 2em;
+}
+
+#sone #edit-profile .profile-field .value {
+       margin-left: inherit;
+}
+
+#sone .profile-field .edit-field-name, #sone .profile-field .move-up-field, #sone .profile-field .move-down-field, #sone .profile-field .delete-field-name {
+       float: right;
+       margin-top: -1ex;
 }
 
 #sone #tail {
@@ -451,6 +498,10 @@ textarea {
        color: #888;
 }
 
+#sone #tail #version-information {
+       margin-top: 1em;
+}
+
 #sone #add-sone textarea, #sone #create-sone textarea, #sone #load-sone textarea, #sone #edit-profile textarea {
        height: 1.5em;
 }
@@ -521,6 +572,6 @@ textarea {
 }
 
 #sone .confirm {
-       font-weight: bold !important;
-       color: red !important;
+       font-weight: bold;
+       color: red;
 }
index ef34c66..80d039c 100644 (file)
@@ -545,7 +545,7 @@ function ajaxifyPost(postElement) {
                        postReply(postId, text, function(success, error, replyId) {
                                if (success) {
                                        $(inputField).val("");
-                                       loadNewReply(replyId);
+                                       loadNewReply(replyId, getCurrentSoneId(), postId);
                                        markPostAsKnown(getPostElement(inputField));
                                        $("#sone .post#" + postId + " .create-reply").addClass("hidden");
                                } else {
@@ -606,7 +606,10 @@ function ajaxifyPost(postElement) {
        });
 
        /* mark everything as known on click. */
-       $(postElement).click(function() {
+       $(postElement).click(function(event) {
+               if ($(event.target).hasClass("click-to-show")) {
+                       return false;
+               }
                markPostAsKnown(this);
        });
 
@@ -705,11 +708,11 @@ function getStatus() {
                        });
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
-                               loadNewPost(value);
+                               loadNewPost(value.id, value.sone, value.recipient, value.time);
                        });
                        /* process new replies. */
                        $.each(data.newReplies, function(index, value) {
-                               loadNewReply(value);
+                               loadNewReply(value.id, value.sone, value.post, value.postSone);
                        });
                        /* do it again in 5 seconds. */
                        setTimeout(getStatus, 5000);
@@ -724,6 +727,16 @@ function getStatus() {
 }
 
 /**
+ * Returns the ID of the currently logged in Sone.
+ *
+ * @return The ID of the current Sone, or an empty string if no Sone is logged
+ *         in
+ */
+function getCurrentSoneId() {
+       return $("#currentSoneId").text();
+}
+
+/**
  * Returns the content of the page-id attribute.
  *
  * @returns The page ID
@@ -816,10 +829,20 @@ function hasReply(replyId) {
        return $("#sone .reply#" + replyId).length > 0;
 }
 
-function loadNewPost(postId) {
+function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
+       if (!isIndexPage()) {
+               if (!isViewPostPage() || (getShownPostId() != postId)) {
+                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+                               return;
+                       }
+               }
+       }
+       if (getPostTime($("#sone .post").last()) > time) {
+               return;
+       }
        $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
@@ -838,8 +861,6 @@ function loadNewPost(postId) {
                        newPost = $(data.post.html).addClass("hidden");
                        if (firstOlderPost != null) {
                                newPost.insertBefore(firstOlderPost);
-                       } else {
-                               $("#sone #posts").append(newPost);
                        }
                        ajaxifyPost(newPost);
                        newPost.slideDown();
@@ -848,10 +869,13 @@ function loadNewPost(postId) {
        });
 }
 
-function loadNewReply(replyId) {
+function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (hasReply(replyId)) {
                return;
        }
+       if (!hasPost(postId)) {
+               return;
+       }
        $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
@@ -892,6 +916,7 @@ function markPostAsKnown(postElements) {
                        (function(postElement) {
                                $.getJSON("markPostAsKnown.ajax", {"formPassword": getFormPassword(), "post": getPostId(postElement)}, function(data, textStatus) {
                                        $(postElement).removeClass("new");
+                                       $(".click-to-show", postElement).removeClass("new");
                                });
                        })(postElement);
                }
@@ -961,6 +986,80 @@ function showNotificationDetails(notificationId) {
        $("#sone .notification#" + notificationId + " .short-text").hide();
 }
 
+/**
+ * Deletes the field with the given ID from the profile.
+ *
+ * @param fieldId
+ *            The ID of the field to delete
+ */
+function deleteProfileField(fieldId) {
+       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+               if (data && data.success) {
+                       $("#sone .profile-field#" + data.field.id).slideUp();
+               }
+       });
+}
+
+/**
+ * Renames a profile field.
+ *
+ * @param fieldId
+ *            The ID of the field to rename
+ * @param newName
+ *            The new name of the field
+ * @param successFunction
+ *            Called when the renaming was successful
+ */
+function editProfileField(fieldId, newName, successFunction) {
+       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID one slot in the given direction.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param direction
+ *            The direction to move in (“up” or “down”)
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileField(fieldId, direction, successFunction) {
+       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID up one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldUp(fieldId, successFunction) {
+       moveProfileField(fieldId, "up", successFunction);
+}
+
+/**
+ * Moves the profile field with the given ID down one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldDown(fieldId, successFunction) {
+       moveProfileField(fieldId, "down", successFunction);
+}
+
 //
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
@@ -979,7 +1078,7 @@ $(document).ready(function() {
                        text = $(this).find(":input:enabled").val();
                        $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
-                                       loadNewPost(data.postId);
+                                       loadNewPost(data.postId, getCurrentSoneId());
                                }
                        });
                        $(this).find(":input:enabled").val("").blur();
@@ -994,7 +1093,7 @@ $(document).ready(function() {
                        text = $(this).find(":input:enabled").val();
                        $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
-                                       loadNewPost(data.postId);
+                                       loadNewPost(data.postId, getCurrentSoneId());
                                }
                        });
                        $(this).find(":input:enabled").val("").blur();
diff --git a/src/main/resources/templates/deleteProfileField.html b/src/main/resources/templates/deleteProfileField.html
new file mode 100644 (file)
index 0000000..006e1ff
--- /dev/null
@@ -0,0 +1,19 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteProfileField.Text|l10n|html></p>
+
+       <div class="profile-field">
+               <div class="name"><% field.name|html></div>
+               <div class="value"><% field.value|html></div>
+       </div>
+
+       <form id="delete-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <button type="submit" name="confirm" value="true"><%= Page.DeleteProfileField.Button.Yes|l10n|html></button>
+               <button type="submit" name="cancel" value="true"><%= Page.DeleteProfileField.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
index d1d5943..8a1b7d7 100644 (file)
@@ -1,7 +1,14 @@
 <%include include/head.html>
 
        <script language="javascript">
-               $(document).ready(function() {
+               function recheckMoveButtons() {
+                       $("#sone .profile-field").each(function() {
+                               $(".move-up-field", this).toggleClass("hidden", $(this).prev(".profile-field").length == 0);
+                               $(".move-down-field", this).toggleClass("hidden", $(this).next(".profile-field").length == 0);
+                       });
+               }
+
+               $(function() {
                        getTranslation("WebInterface.DefaultText.FirstName", function(firstNameDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=first-name]", firstNameDefaultText, "first-name", true, true);
                        });
                        getTranslation("WebInterface.DefaultText.BirthYear", function(birthYearDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=birth-year]", birthYearDefaultText, "birth-year", true, true);
                        });
+                       getTranslation("WebInterface.DefaultText.FieldName", function(fieldNameDefaultText) {
+                               registerInputTextareaSwap("#sone #add-profile-field input[name=field-name]", fieldNameDefaultText, "field-name", true, true);
+                       });
+
+                       <%foreach fields field>
+                               registerInputTextareaSwap("#sone #edit-profile input[name=field-<% loop.count>]", <% field.key|js>, "field-<% loop.count>", true, true);
+                       <%/foreach>
 
                        /* hide all the labels. */
-                       $("#sone #edit-profile label").hide();
+                       $("#sone #edit-profile label, #sone #add-profile-field label").hide();
+
+                       /* ajaxify the delete buttons. */
+                       getTranslation("Page.EditProfile.Fields.Button.ReallyDelete", function(reallyDeleteText) {
+                               $("#sone #edit-profile .delete-field-name button").each(function() {
+                                       confirmButton = $(this).clone().addClass("hidden").addClass("confirm").text(reallyDeleteText).insertAfter(this);
+                                       (function(deleteButton, confirmButton) {
+                                               deleteButton.click(function() {
+                                                       deleteButton.fadeOut("slow", function() {
+                                                               confirmButton.fadeIn("slow");
+                                                               $(document).one("click", function() {
+                                                                       if (this != confirmButton.get(0)) {
+                                                                               confirmButton.fadeOut("slow", function() {
+                                                                                       deleteButton.fadeIn("slow");
+                                                                               });
+                                                                       }
+                                                                       return false;
+                                                               });
+                                                       });
+                                                       return false;
+                                               });
+                                               confirmButton.click(function() {
+                                                       confirmButton.fadeOut("slow");
+                                                       buttonName = confirmButton.attr("name");
+                                                       fieldId = buttonName.substring("delete-field-".length);
+                                                       deleteProfileField(fieldId);
+                                                       recheckMoveButtons();
+                                                       return false;
+                                               });
+                                       })($(this), confirmButton);
+                               });
+                       });
+
+                       /* ajaxify the edit button. */
+                       $("#sone #edit-profile .edit-field-name button").each(function() {
+                               profileField = $(this).parents(".profile-field");
+                               fieldNameElement = profileField.find(".name");
+                               inputField = $("input[type=text].short", profileField);
+                               confirmButton = $("button.confirm", profileField);
+                               cancelButton = $("button.cancel", profileField);
+                               (function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                       cleanUp = function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                               editButton.removeAttr("disabled");
+                                               inputField.addClass("hidden");
+                                               confirmButton.addClass("hidden");
+                                               cancelButton.addClass("hidden");
+                                               fieldNameElement.removeClass("hidden");
+                                       };
+                                       confirmButton.click(function() {
+                                               inputField.attr("disabled", "disabled");
+                                               confirmButton.attr("disabled", "disabled");
+                                               cancelButton.attr("disabled", "disabled");
+                                               editProfileField(confirmButton.parents(".profile-field").attr("id"), inputField.val(), function() {
+                                                       fieldNameElement.text(inputField.val());
+                                                       cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               });
+                                               return false;
+                                       });
+                                       cancelButton.click(function() {
+                                               cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               return false;
+                                       });
+                                       inputField.keypress(function(event) {
+                                               if (event.which == 13) {
+                                                       confirmButton.click();
+                                                       return false;
+                                               } else if (event.which == 27) {
+                                                       cancelButton.click();
+                                                       return false;
+                                               }
+                                       });
+                                       editButton.click(function() {
+                                               editButton.attr("disabled", "disabled");
+                                               fieldNameElement.addClass("hidden");
+                                               inputField.removeAttr("disabled").val(fieldNameElement.text()).removeClass("hidden").focus().select();
+                                               confirmButton.removeAttr("disabled").removeClass("hidden");
+                                               cancelButton.removeAttr("disabled").removeClass("hidden");
+                                               return false;
+                                       });
+                               })($(this), inputField, confirmButton, cancelButton, fieldNameElement);
+                       });
+
+                       /* ajaxify “move up” and “move down” buttons. */
+                       $("#sone .profile-field .move-down-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldDown(profileField.attr("id"), function() {
+                                       next = profileField.next();
+                                       current = profileField.insertAfter(next);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
+                       $("#sone .profile-field .move-up-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldUp(profileField.attr("id"), function() {
+                                       previous = profileField.prev();
+                                       current = profileField.insertBefore(previous);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
                });
        </script>
 
        <p><%= Page.EditProfile.Page.Description|l10n|html></p>
        <p><%= Page.EditProfile.Page.Hint.Optionality|l10n|html></p>
 
-       <%if changed>
-               <p><%= Page.EditProfile.Page.Status.Changed|l10n|html></p>
-       <%/if>
-
        <form id="edit-profile" method="post">
                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
 
                </div>
 
                <div>
-                       <button type="submit"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+               </div>
+
+               <h1><%= Page.EditProfile.Fields.Title|l10n|html></h1>
+
+               <p><%= Page.EditProfile.Fields.Description|l10n|html></p>
+
+               <%foreach fields field fieldLoop>
+                       <div class="profile-field" id="<% field.id|html>">
+                               <div class="name"><% field.name|html></div>
+                               <input class="short hidden" type="text"><button class="confirm hidden" type="button">✔</button><button class="cancel hidden" type="button">✘</button>
+                               <div class="edit-field-name"><button type="submit" name="edit-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Edit|l10n|html></button></div>
+                               <div class="delete-field-name"><button type="submit" name="delete-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Delete|l10n|html></button></div>
+                               <div class="<%if fieldLoop.last>hidden <%/if>move-down-field"><button type="submit" name="move-down-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveDown|l10n|html></button></div>
+                               <div class="<%if fieldLoop.first>hidden <%/if>move-up-field"><button type="submit" name="move-up-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveUp|l10n|html></button></div>
+                               <div class="value"><input type="text" name="field-<% field.id|html>" value="<% field.value|html>" /></div>
+                       </div>
+
+                       <%if fieldLoop.last>
+                               <div>
+                                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                               </div>
+                       <%/if>
+               <%/foreach>
+
+       </form>
+
+       <form id="add-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+
+               <a name="profile-fields"></a>
+               <h2><%= Page.EditProfile.Fields.AddField.Title|l10n|html></h2>
+
+               <%if duplicateFieldName>
+                       <p><%= Page.EditProfile.Error.DuplicateFieldName|l10n|replace needle="{fieldName}" replacementKey="fieldName"|html></p>
+               <%/if>
+
+               <div id="new-field">
+                       <label for="new-field"><%= Page.EditProfile.Fields.AddField.Label.Name|l10n|html></label>
+                       <input type="text" name="field-name" value="" />
+                       <button type="submit" name="add-field" value="true"><%= Page.EditProfile.Fields.AddField.Button.AddField|l10n|html></button>
                </div>
 
        </form>
diff --git a/src/main/resources/templates/editProfileField.html b/src/main/resources/templates/editProfileField.html
new file mode 100644 (file)
index 0000000..6030f71
--- /dev/null
@@ -0,0 +1,24 @@
+<%include include/head.html>
+
+       <h1><%= Page.EditProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.EditProfileField.Text|l10n|html></p>
+
+       <%if duplicateFieldName>
+               <p><%= Page.EditProfileField.Error.DuplicateFieldName|l10n|html></p>
+       <%/if>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <div>
+                       <input type="text" name="name" value="<% field.name|html>" />
+                       <button type="submit" name="save" value="true"><%= Page.EditProfileField.Button.Save|l10n|html></button>
+               </div>
+               <p>
+                       <button type="reset"><%= Page.EditProfileField.Button.Reset|l10n|html></button>
+                       <button type="submit" name="cancel" value="true"><%= Page.EditProfileField.Button.Cancel|l10n|html></button>
+               </p>
+       </form>
+
+<%include include/tail.html>
index 1d8e284..2c2cf3f 100644 (file)
@@ -1,6 +1,7 @@
 <div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
 
        <div id="formPassword"><% formPassword|html></div>
+       <div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
 
        <script src="javascript/jquery-1.4.2.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
index 180bcf2..dd2e25e 100644 (file)
@@ -7,6 +7,13 @@
                                <img src="images/flattr-badge-large.png" alt="Flattr Sone" title="Flattr Sone" />
                        </a>
                </div>
+
+               <div id="version-information">
+                       <div class="current-version"><%= WebInterface.VersionInformation.CurrentVersion|l10n|html> <b><% currentVersion|html></b></div>
+                       <%if hasLatestVersion>
+                               <div class="latest-version"><%= WebInterface.VersionInformation.LatestVersion|l10n|html> <b><% latestVersion|html></b></div>
+                       <%/if>
+               </div>
        </div>
 
 </div>
index 39d53f1..345f376 100644 (file)
@@ -5,8 +5,6 @@
        <h1><%= Page.Index.PostList.Title|l10n|html></h1>
 
        <div id="posts">
-               <%getpage>
-               <%paginate list=posts pagesize=25>
                <%= page|store key=pageParameter>
                <%include include/pagination.html>
                <%foreach pagination.items post>
index 7d3c8aa..9ba037f 100644 (file)
                <birth-day><% currentSone.profile.birthDay|xml></birth-day>
                <birth-month><% currentSone.profile.birthMonth|xml></birth-month>
                <birth-year><% currentSone.profile.birthYear|xml></birth-year>
+               <fields>
+                       <%foreach currentSone.profile.fields field>
+                       <field>
+                               <field-name><% field.key|xml></field-name>
+                               <field-value><% field.value|xml></field-value>
+                       </field>
+                       <%/foreach>
+               </fields>
        </profile>
 
        <posts>
diff --git a/src/main/resources/templates/invalid.html b/src/main/resources/templates/invalid.html
new file mode 100644 (file)
index 0000000..d838134
--- /dev/null
@@ -0,0 +1,7 @@
+<%include include/head.html>
+
+       <h1><%= Page.Invalid.Page.Title|l10n|html></h1>
+
+       <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+
+<%include include/tail.html>
index f17a1ba..94f7a1f 100644 (file)
@@ -1,6 +1,6 @@
 <div class="short-text">
        <%= Notification.NewPost.ShortText|l10n|html>
-       <a href="javascript:showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text hidden">
        <%= Notification.NewPost.Text|l10n|html>
index 0cf203b..d6d0670 100644 (file)
@@ -1,6 +1,6 @@
 <div class="short-text">
        <%= Notification.NewReply.ShortText|l10n|html>
-       <a href="javascript:showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text hidden">
        <%= Notification.NewReply.Text|l10n|html>
index ec715ae..8389f7a 100644 (file)
@@ -1,6 +1,6 @@
 <div class="short-text">
        <%= Notification.NewSone.ShortText|l10n|html>
-       <a href="javascript:showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text hidden">
        <%= Notification.NewSone.Text|l10n|html>
diff --git a/src/main/resources/templates/notify/newVersionNotification.html b/src/main/resources/templates/notify/newVersionNotification.html
new file mode 100644 (file)
index 0000000..23a5855
--- /dev/null
@@ -0,0 +1 @@
+<div class="text"><%= Notification.NewVersion.Text|l10n|html|replace needle="{version}" replacementKey=version></div>
index 884fa0f..c6dadf7 100644 (file)
 
                <%if ! sone.current>
                        <%include include/viewSone.html>
+               <%/if>
+
+               <h1><%= Page.ViewSone.Profile.Title|l10n|html></h1>
 
+                       <div class="profile-field">
+                               <div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
+                               <div class="value"><% sone.niceName|html></div>
+                       </div>
+
+                       <%foreach sone.profile.fields field>
+                               <div class="profile-field">
+                                       <div class="name"><% field.name|html></div>
+                                       <div class="value"><% field.value|html></div>
+                               </div>
+                       <%/foreach>
+
+               <%if ! sone.current>
                        <p><%= Page.ViewSone.WriteAMessage|l10n|html></p>
 
                        <form action="createPost.html" id="post-message" method="post">
@@ -31,7 +47,6 @@
                        </form>
                <%/if>
 
-
                <h1><%= Page.ViewSone.PostList.Title|l10n|insert needle="{sone}" key=sone.niceName|html></h1>
 
                <div id="posts">