This fixes #21.
<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.6</version>
+ <version>0.6.4</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<artifactId>utils</artifactId>
- <version>0.9.3-SNAPSHOT</version>
+ <version>0.9.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.pterodactylus.util.config.ConfigurationException;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.validation.IntegerRangeValidator;
import net.pterodactylus.util.validation.Validation;
import net.pterodactylus.util.version.Version;
import freenet.keys.FreenetURI;
/** The Sone downloader. */
private final SoneDownloader soneDownloader;
+ /** Sone downloader thread-pool. */
+ private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
+
/** The update checker. */
private final UpdateChecker updateChecker;
return null;
}
Sone sone = addLocalSone(ownIdentity);
+ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+ sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
+ saveSone(sone);
return sone;
}
remoteSones.put(identity.getId(), sone);
soneDownloader.addSone(sone);
setSoneStatus(sone, SoneStatus.unknown);
- new Thread(new Runnable() {
+ soneDownloaders.execute(new Runnable() {
@Override
@SuppressWarnings("synthetic-access")
public void run() {
- soneDownloader.fetchSone(sone);
+ soneDownloader.fetchSone(sone, sone.getRequestUri());
}
- }, "Sone Downloader").start();
+ });
return sone;
}
}
return;
}
+ /* initialize options. */
+ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+
/* load Sone. */
String sonePrefix = "Sone/" + sone.getId();
Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
}
/* load options. */
- sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
/* if we’re still here, Sone was loaded successfully. */
synchronized (posts) {
posts.remove(post.getId());
}
+ coreListenerManager.firePostRemoved(post);
synchronized (newPosts) {
markPostKnown(post);
knownPosts.remove(post.getId());
configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
+ configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
@SuppressWarnings("unchecked")
private void loadConfiguration() {
/* create options. */
- options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
+ options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
@Override
public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
}
}));
- options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(25));
- options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
- options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
+ options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+ options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
+ options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
+ options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
return;
}
- options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
- options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
- options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
- options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+ loadConfigurationValue("InsertionDelay");
+ loadConfigurationValue("PostsPerPage");
+ options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
+ loadConfigurationValue("PositiveTrust");
+ loadConfigurationValue("NegativeTrust");
options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
}
/**
+ * Loads an {@link Integer} configuration value for the option with the
+ * given name, logging validation failures.
+ *
+ * @param optionName
+ * The name of the option to load
+ */
+ private void loadConfigurationValue(String optionName) {
+ try {
+ options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
+ } catch (IllegalArgumentException iae1) {
+ logger.log(Level.WARNING, "Invalid value for " + optionName + " in configuration, using default.");
+ }
+ }
+
+ /**
* Generate a Sone URI from the given URI and latest edition.
*
* @param uriString
public void run() {
Sone sone = getRemoteSone(identity.getId());
sone.setIdentity(identity);
+ sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
soneDownloader.addSone(sone);
soneDownloader.fetchSone(sone);
}
@Override
public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
trustedIdentities.get(ownIdentity).remove(identity);
+ boolean foundIdentity = false;
+ for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
+ if (trustedIdentity.getKey().equals(ownIdentity)) {
+ continue;
+ }
+ if (trustedIdentity.getValue().contains(identity)) {
+ foundIdentity = true;
+ }
+ }
+ if (foundIdentity) {
+ /* some local identity still trusts this identity, don’t remove. */
+ return;
+ }
+ Sone sone = getSone(identity.getId(), false);
+ if (sone == null) {
+ /* TODO - we don’t have the Sone anymore. should this happen? */
+ return;
+ }
+ synchronized (posts) {
+ synchronized (newPosts) {
+ for (Post post : sone.getPosts()) {
+ posts.remove(post.getId());
+ newPosts.remove(post.getId());
+ coreListenerManager.firePostRemoved(post);
+ }
+ }
+ }
+ synchronized (replies) {
+ synchronized (newReplies) {
+ for (Reply reply : sone.getReplies()) {
+ replies.remove(reply.getId());
+ newReplies.remove(reply.getId());
+ coreListenerManager.fireReplyRemoved(reply);
+ }
+ }
+ }
+ synchronized (remoteSones) {
+ remoteSones.remove(identity.getId());
+ }
+ synchronized (newSones) {
+ newSones.remove(identity.getId());
+ }
}
//
}
/**
+ * Validates the given insertion delay.
+ *
+ * @param insertionDelay
+ * The insertion delay to validate
+ * @return {@code true} if the given insertion delay was valid, {@code
+ * false} otherwise
+ */
+ public boolean validateInsertionDelay(Integer insertionDelay) {
+ return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+ }
+
+ /**
* Sets the insertion delay
*
* @param insertionDelay
}
/**
+ * Validates the number of posts per page.
+ *
+ * @param postsPerPage
+ * The number of posts per page
+ * @return {@code true} if the number of posts per page was valid,
+ * {@code false} otherwise
+ */
+ public boolean validatePostsPerPage(Integer postsPerPage) {
+ return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+ }
+
+ /**
* Sets the number of posts to show per page.
*
* @param postsPerPage
}
/**
+ * Returns whether Sone requires full access to be even visible.
+ *
+ * @return {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public boolean isRequireFullAccess() {
+ return options.getBooleanOption("RequireFullAccess").get();
+ }
+
+ /**
+ * Sets whether Sone requires full access to be even visible.
+ *
+ * @param requireFullAccess
+ * {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public void setRequireFullAccess(Boolean requireFullAccess) {
+ options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+ }
+
+ /**
* Returns the positive trust.
*
* @return The positive trust
}
/**
+ * Validates the positive trust.
+ *
+ * @param positiveTrust
+ * The positive trust to validate
+ * @return {@code true} if the positive trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validatePositiveTrust(Integer positiveTrust) {
+ return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+ }
+
+ /**
* Sets the positive trust.
*
* @param positiveTrust
}
/**
+ * Validates the negative trust.
+ *
+ * @param negativeTrust
+ * The negative trust to validate
+ * @return {@code true} if the negative trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validateNegativeTrust(Integer negativeTrust) {
+ return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+ }
+
+ /**
* Sets the negative trust.
*
* @param negativeTrust
/*
- * FreenetSone - FreenetInterface.java - Copyright © 2010 David Roden
+ * Sone - FreenetInterface.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
+/*
+ * Sone - Options.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
package net.pterodactylus.sone.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import net.pterodactylus.util.validation.Validator;
+
/**
* Stores various options that influence Sone’s behaviour.
*
public T getReal();
/**
+ * Validates the given value. Note that {@code null} is always a valid
+ * value!
+ *
+ * @param value
+ * The value to validate
+ * @return {@code true} if this option does not have a {@link Validator}
+ * , or the {@link Validator} validates this object, {@code
+ * false} otherwise
+ */
+ public boolean validate(T value);
+
+ /**
* Sets the current value of the option.
*
* @param value
* The new value of the option
+ * @throws IllegalArgumentException
+ * if the value is not valid for this option
*/
- public void set(T value);
+ public void set(T value) throws IllegalArgumentException;
}
/** The current value. */
private volatile T value;
+ /** The validator. */
+ private Validator<T> validator;
+
/** The option watcher. */
private final List<OptionWatcher<T>> optionWatchers = new ArrayList<OptionWatcher<T>>();
* The option watchers
*/
public DefaultOption(T defaultValue, OptionWatcher<T>... optionWatchers) {
+ this(defaultValue, null, optionWatchers);
+ }
+
+ /**
+ * Creates a new default option.
+ *
+ * @param defaultValue
+ * The default value of the option
+ * @param validator
+ * The validator for value validation
+ * @param optionWatchers
+ * The option watchers
+ */
+ public DefaultOption(T defaultValue, Validator<T> validator, OptionWatcher<T>... optionWatchers) {
this.defaultValue = defaultValue;
+ this.validator = validator;
this.optionWatchers.addAll(Arrays.asList(optionWatchers));
}
/**
* {@inheritDoc}
*/
+ public boolean validate(T value) {
+ return (validator == null) || (value == null) || validator.validate(value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public void set(T value) {
+ if ((value != null) && (validator != null) && (!validator.validate(value))) {
+ throw new IllegalArgumentException("New Value (" + value + ") could not be validated.");
+ }
T oldValue = this.value;
this.value = value;
if (!get().equals(oldValue)) {
* The Sone to fetch
*/
public void fetchSone(Sone sone) {
- fetchSone(sone, sone.getRequestUri());
+ fetchSone(sone, sone.getRequestUri().sskForUSK());
}
/**
/*
- * FreenetSone - SoneException.java - Copyright © 2010 David Roden
+ * Sone - SoneException.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - SoneInserter.java - Copyright © 2010 David Roden
+ * Sone - SoneInserter.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
synchronized (sone) {
if (lastInsertFingerprint.equals(sone.getFingerprint())) {
logger.log(Level.FINE, "Sone “%s” was not modified further, resetting counter…", new Object[] { sone });
- core.saveSone(sone);
lastModificationTime = 0;
modified = false;
}
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private static class InsertInformation {
+ private class InsertInformation {
/** All properties of the Sone, copied for thread safety. */
private final Map<String, Object> soneProperties = new HashMap<String, Object>();
TemplateContext templateContext = templateContextFactory.createTemplateContext();
templateContext.set("currentSone", soneProperties);
+ templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
templateContext.set("version", SonePlugin.VERSION);
StringWriter writer = new StringWriter();
StringBucket bucket = null;
private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
/** The current latest known edition. */
- private static final int LATEST_EDITION = 25;
+ private static final int LATEST_EDITION = 36;
/** The Freenet interface. */
private final FreenetInterface freenetInterface;
/*
- * FreenetSone - StatusUpdate.java - Copyright © 2010 David Roden
+ * Sone - Post.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - Profile.java - Copyright © 2010 David Roden
+ * Sone - Profile.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - Sone.java - Copyright © 2010 David Roden
+ * Sone - Sone.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - L10nFilter.java - Copyright © 2010 David Roden
+ * Sone - L10nFilter.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - PluginStoreConfigurationBackend.java - Copyright © 2010 David Roden
+ * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - TemplateBucket.java - Copyright © 2010 David Roden
+ * Sone - StringBucket.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
import java.util.EventListener;
-
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
/** Cached trust. */
+ /* synchronize on itself. */
private final WritableCache<OwnIdentity, Trust> trustCache = new MemoryCache<OwnIdentity, Trust>(new ValueRetriever<OwnIdentity, Trust>() {
@Override
@Override
public Trust getTrust(OwnIdentity ownIdentity) {
try {
- return trustCache.get(ownIdentity);
+ synchronized (trustCache) {
+ return trustCache.get(ownIdentity);
+ }
} catch (CacheException ce1) {
logger.log(Level.WARNING, "Could not get trust for OwnIdentity: " + ownIdentity, ce1);
return null;
* The trust received for this identity
*/
void setTrustPrivate(OwnIdentity ownIdentity, Trust trust) {
- trustCache.put(ownIdentity, trust);
+ synchronized (trustCache) {
+ trustCache.put(ownIdentity, trust);
+ }
}
//
Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
+ Set<OwnIdentity> ownIdentities = null;
+ boolean identitiesLoaded = false;
try {
/* get all identities with the wanted context from WoT. */
- Set<OwnIdentity> ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
+ ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
- /* check for changes. */
- for (OwnIdentity ownIdentity : ownIdentities) {
- currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
- }
- checkOwnIdentities(currentOwnIdentities);
-
- /* now filter for context and get all identities. */
+ /* load trusted identities. */
for (OwnIdentity ownIdentity : ownIdentities) {
if ((context != null) && !ownIdentity.hasContext(context)) {
continue;
}
+ currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context);
Map<String, Identity> identities = new HashMap<String, Identity>();
for (Identity identity : trustedIdentities) {
identities.put(identity.getId(), identity);
}
+ }
+ identitiesLoaded = true;
+ } catch (WebOfTrustException wote1) {
+ logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+ }
+
+ if (identitiesLoaded) {
+
+ /* check for changes. */
+ checkOwnIdentities(currentOwnIdentities);
+
+ /* now check for changes in remote identities. */
+ for (OwnIdentity ownIdentity : currentOwnIdentities.values()) {
/* find new identities. */
for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) {
}
}
}
-
- /* remember the current set of identities. */
- oldIdentities = currentIdentities;
}
- } catch (WebOfTrustException wote1) {
- logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+ /* remember the current set of identities. */
+ oldIdentities = currentIdentities;
}
/* wait a minute before checking again. */
/*
- * FreenetSone - SonePlugin.java - Copyright © 2010 David Roden
+ * Sone - SonePlugin.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
}
/** The version. */
- public static final Version VERSION = new Version(0, 6);
+ public static final Version VERSION = new Version(0, 6, 4);
/** The logger. */
private static final Logger logger = Logging.getLogger(SonePlugin.class);
}
/**
- * Sets the elements to show in this notification.
+ * Sets the elements to show in this notification. This method will not call
+ * {@link #touch()}.
*
* @param elements
* The elements to show
public void setElements(Collection<? extends T> elements) {
this.elements.clear();
this.elements.addAll(elements);
- touch();
}
/**
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.validation.Validation;
/**
* Filter for {@link ListNotification}s.
* The current Sone, or {@code null} if not logged in
* @return The filtered notifications
*/
- public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
- ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
- if (newPostNotification != null) {
- ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
- int notificationIndex = notifications.indexOf(newPostNotification);
- if (filteredNotification == null) {
- notifications.remove(notificationIndex);
- } else {
- notifications.set(notificationIndex, filteredNotification);
- }
- }
- ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
- if (newReplyNotification != null) {
- ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
- int notificationIndex = notifications.indexOf(newReplyNotification);
- if (filteredNotification == null) {
- notifications.remove(notificationIndex);
+ @SuppressWarnings("unchecked")
+ public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
+ List<Notification> filteredNotifications = new ArrayList<Notification>();
+ for (Notification notification : notifications) {
+ if (notification.getId().equals("new-post-notification")) {
+ ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
+ } else if (notification.getId().equals("new-replies-notification")) {
+ ListNotification<Reply> filteredNotification = filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
} else {
- notifications.set(notificationIndex, filteredNotification);
+ filteredNotifications.add(notification);
}
}
- return notifications;
+ return filteredNotifications;
}
/**
* @return The filtered new-post notification, or {@code null} if the
* notification should be removed
*/
- private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
+ public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
if (currentSone == null) {
return null;
}
List<Post> newPosts = new ArrayList<Post>();
for (Post post : newPostNotification.getElements()) {
- if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+ if (isPostVisible(currentSone, post)) {
newPosts.add(post);
}
}
* @return The filtered new-reply notification, or {@code null} if the
* notification should be removed
*/
- private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+ public static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
if (currentSone == null) {
return null;
}
List<Reply> newReplies = new ArrayList<Reply>();
for (Reply reply : newReplyNotification.getElements()) {
- if (currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient())) {
+ if (isReplyVisible(currentSone, reply)) {
newReplies.add(reply);
}
}
}
/**
- * Finds the notification with the given ID in the list of notifications and
- * returns it.
+ * Checks whether a post is visible to the given Sone. A post is not
+ * considered visible if one of the following statements is true:
+ * <ul>
+ * <li>The post does not have a Sone.</li>
+ * <li>The Sone of the post is not the given Sone, the given Sone does not
+ * follow the post’s Sone, and the given Sone is not the recipient of the
+ * post.</li>
+ * <li>The trust relationship between the two Sones can not be retrieved.</li>
+ * <li>The given Sone has explicitely assigned negative trust to the post’s
+ * Sone.</li>
+ * <li>The given Sone has not explicitely assigned negative trust to the
+ * post’s Sone but the implicit trust is negative.</li>
+ * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+ * </ul>
+ * If none of these statements is true the post is considered visible.
*
- * @param <T>
- * The type of the item in the notification
- * @param notifications
- * The notification to search
- * @param notificationId
- * The ID of the requested notification
- * @param notificationElementClass
- * The class of the notification item
- * @return The requested notification, or {@code null} if no notification
- * with the given ID could be found
+ * @param sone
+ * The Sone that checks for a post’s visibility
+ * @param post
+ * The post to check for visibility
+ * @return {@code true} if the post is considered visible, {@code false}
+ * otherwise
*/
- @SuppressWarnings("unchecked")
- private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
- for (Notification notification : notifications) {
- if (!notificationId.equals(notification.getId())) {
- continue;
+ public static boolean isPostVisible(Sone sone, Post post) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Post", post).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+ Sone postSone = post.getSone();
+ if (postSone == null) {
+ return false;
+ }
+ Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+ if (trust != null) {
+ if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+ return false;
+ }
+ if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+ return false;
}
- return (ListNotification<T>) notification;
+ } else {
+ return false;
+ }
+ if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
+ return false;
+ }
+ if (post.getTime() > System.currentTimeMillis()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether a reply is visible to the given Sone. A reply is not
+ * considered visible if one of the following statements is true:
+ * <ul>
+ * <li>The reply does not have a post.</li>
+ * <li>The reply’s post does not have a Sone.</li>
+ * <li>The Sone of the reply’s post is not the given Sone, the given Sone
+ * does not follow the reply’s post’s Sone, and the given Sone is not the
+ * recipient of the reply’s post.</li>
+ * <li>The trust relationship between the two Sones can not be retrieved.</li>
+ * <li>The given Sone has explicitely assigned negative trust to the post’s
+ * Sone.</li>
+ * <li>The given Sone has not explicitely assigned negative trust to the
+ * reply’s post’s Sone but the implicit trust is negative.</li>
+ * <li>The reply’s post’s {@link Post#getTime() time} is in the future.</li>
+ * <li>The reply’s {@link Reply#getTime() time} is in the future.</li>
+ * </ul>
+ * If none of these statements is true the reply is considered visible.
+ *
+ * @param sone
+ * The Sone that checks for a post’s visibility
+ * @param reply
+ * The reply to check for visibility
+ * @return {@code true} if the reply is considered visible, {@code false}
+ * otherwise
+ */
+ public static boolean isReplyVisible(Sone sone, Reply reply) {
+ Validation.begin().isNotNull("Sone", sone).isNotNull("Reply", reply).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+ Post post = reply.getPost();
+ if (post == null) {
+ return false;
+ }
+ if (!isPostVisible(sone, post)) {
+ return false;
+ }
+ if (reply.getTime() > System.currentTimeMillis()) {
+ return false;
}
- return null;
+ return true;
}
}
+++ /dev/null
-/*
- * Sone - NotificationManagerAccessor.java - Copyright © 2010 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.template;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * Adds additional properties to a {@link NotificationManager}.
- * <dl>
- * <dd>all</dd>
- * <dt>Returns all notifications, sorted by creation time, oldest first.</dt>
- * <dd>new</dd>
- * <dt>Returns all changed notifications, sorted by last updated time, newest
- * first.</dt>
- * </dl>
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NotificationManagerAccessor extends ReflectionAccessor {
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Object get(TemplateContext templateContext, Object object, String member) {
- NotificationManager notificationManager = (NotificationManager) object;
- if ("all".equals(member)) {
- List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
- Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
- return notifications;
- }
- return super.get(templateContext, object, member);
- }
-
-}
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.text.FreenetLinkParser;
import net.pterodactylus.sone.text.FreenetLinkParserContext;
+import net.pterodactylus.sone.web.page.Page.Request;
import net.pterodactylus.util.template.Filter;
import net.pterodactylus.util.template.TemplateContext;
import net.pterodactylus.util.template.TemplateContextFactory;
if (sone == null) {
sone = core.getSone(soneKey, false);
}
- FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
+ FreenetLinkParserContext context = new FreenetLinkParserContext((Request) templateContext.get("request"), sone);
try {
return linkParser.parse(context, new StringReader(text));
} catch (IOException ioe1) {
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Accessor;
import net.pterodactylus.util.template.ReflectionAccessor;
return core.isNewSone(sone.getId());
} else if (member.equals("locked")) {
return core.isLocked(sone);
+ } else if (member.equals("lastUpdatedText")) {
+ return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), System.currentTimeMillis() - sone.getTime());
} else if (member.equals("trust")) {
Sone currentSone = (Sone) templateContext.get("currentSone");
if (currentSone == null) {
} else if (linkType == LinkType.POST) {
String postId = link.substring(7);
Post post = core.getPost(postId, false);
- if (post != null) {
+ if ((post != null) && (post.getSone() != null)) {
String postText = post.getText();
postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
Sone postSone = post.getSone();
package net.pterodactylus.sone.text;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request;
/**
* {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
*/
public class FreenetLinkParserContext implements ParserContext {
+ /** The request being processed. */
+ private final Request request;
+
/** The posting Sone. */
private final Sone postingSone;
/**
* Creates a new link parser context.
*
+ * @param request
+ * The request being processed
* @param postingSone
* The posting Sone
*/
- public FreenetLinkParserContext(Sone postingSone) {
+ public FreenetLinkParserContext(Request request, Sone postingSone) {
+ this.request = request;
this.postingSone = postingSone;
}
/**
+ * Returns the request that is currently being processed.
+ *
+ * @return The request being processed
+ */
+ public Request getRequest() {
+ return request;
+ }
+
+ /**
* Returns the Sone that provided the text that is being parsed.
*
* @return The posting Sone
+/*
+ * Sone - ParserContext.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
package net.pterodactylus.sone.text;
--- /dev/null
+/*
+ * Sone - TextFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import java.util.logging.Logger;
+
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * Filter for newly inserted text. This filter strips HTTP links to the local
+ * node of identifying marks, e.g. a link to “http://localhost:8888/KSK@gpl.txt”
+ * will be converted to “KSK@gpl.txt”. This will only work for links that point
+ * to the same address Sone is accessed by, so if you access Sone using
+ * localhost:8888, links to 127.0.0.1:8888 will not be removed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TextFilter {
+
+ /** The logger. */
+ private static final Logger logger = Logging.getLogger(TextFilter.class);
+
+ /**
+ * Filters the given text, stripping the host header part for links to the
+ * local node.
+ *
+ * @param hostHeader
+ * The host header from the request
+ * @param text
+ * The text to filter
+ * @return The filtered text
+ */
+ public static String filter(String hostHeader, String text) {
+
+ /* filter http(s) links to own node. */
+ if (hostHeader != null) {
+ String line = text;
+ for (String toRemove : new String[] { "http://" + hostHeader + "/", "https://" + hostHeader + "/", "http://" + hostHeader, "https://" + hostHeader }) {
+ while (line.indexOf(toRemove) != -1) {
+ line = line.replace(toRemove, "");
+ }
+ }
+ return line;
+ }
+
+ /* not modified. */
+ return text;
+ }
+
+}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
import net.pterodactylus.sone.web.page.Page.Request.Method;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
sender = currentSone;
}
Sone recipient = webInterface.getCore().getSone(recipientId, false);
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text);
throw new RedirectException(returnPage);
}
/*
- * FreenetSone - CreateSonePage.java - Copyright © 2010 David Roden
+ * Sone - CreateSonePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
}
/*
- * FreenetSone - DeleteSonePage.java - Copyright © 2010 David Roden
+ * Sone - DeleteSonePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * FreenetSone - IndexPage.java - Copyright © 2010 David Roden
+ * Sone - IndexPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
@Override
protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
- Sone currentSone = getCurrentSone(request.getToadletContext());
+ final Sone currentSone = getCurrentSone(request.getToadletContext());
List<Post> allPosts = new ArrayList<Post>();
allPosts.addAll(currentSone.getPosts());
for (String friendSoneId : currentSone.getFriends()) {
}
}
}
+ allPosts = Filters.filteredList(allPosts, new Filter<Post>() {
+
+ @Override
+ public boolean filterObject(Post post) {
+ return ListNotificationFilters.isPostVisible(currentSone, post);
+ }
+ });
allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
Collections.sort(allPosts, Post.TIME_COMPARATOR);
Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
- String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+ String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
Sone currentSone = getCurrentSone(request.getToadletContext());
/*
- * FreenetSone - LoginPage.java - Copyright © 2010 David Roden
+ * Sone - LoginPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return getCurrentSone(toadletContext, false) == null;
}
/*
- * FreenetSone - LogoutPage.java - Copyright © 2010 David Roden
+ * Sone - LogoutPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return (getCurrentSone(toadletContext, false) != null) && (webInterface.getCore().getLocalSones().size() != 1);
}
/*
- * Sone - MarkReadPage.java - Copyright © 2011 David Roden
+ * Sone - MarkAsKnownPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
package net.pterodactylus.sone.web;
+import java.util.ArrayList;
+import java.util.List;
+
import net.pterodactylus.sone.core.Core.Preferences;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
Preferences preferences = webInterface.getCore().getPreferences();
Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
if (request.getMethod() == Method.POST) {
+ List<String> fieldErrors = new ArrayList<String>();
if (currentSone != null) {
boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
webInterface.getCore().saveSone(currentSone);
}
Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
- preferences.setInsertionDelay(insertionDelay);
+ if (!preferences.validateInsertionDelay(insertionDelay)) {
+ fieldErrors.add("insertion-delay");
+ } else {
+ preferences.setInsertionDelay(insertionDelay);
+ }
Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
- preferences.setPostsPerPage(postsPerPage);
+ if (!preferences.validatePostsPerPage(postsPerPage)) {
+ fieldErrors.add("posts-per-page");
+ } else {
+ preferences.setPostsPerPage(postsPerPage);
+ }
+ boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
+ preferences.setRequireFullAccess(requireFullAccess);
Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
- preferences.setPositiveTrust(positiveTrust);
+ if (!preferences.validatePositiveTrust(positiveTrust)) {
+ fieldErrors.add("positive-trust");
+ } else {
+ preferences.setPositiveTrust(positiveTrust);
+ }
Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
- preferences.setNegativeTrust(negativeTrust);
+ if (!preferences.validateNegativeTrust(negativeTrust)) {
+ fieldErrors.add("negative-trust");
+ } else {
+ preferences.setNegativeTrust(negativeTrust);
+ }
String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
if (trustComment.trim().length() == 0) {
trustComment = null;
boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
webInterface.getCore().saveConfiguration();
- throw new RedirectException(getPath());
+ if (fieldErrors.isEmpty()) {
+ throw new RedirectException(getPath());
+ }
+ templateContext.set("fieldErrors", fieldErrors);
}
if (currentSone != null) {
templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
}
templateContext.set("insertion-delay", preferences.getInsertionDelay());
templateContext.set("posts-per-page", preferences.getPostsPerPage());
+ templateContext.set("require-full-access", preferences.isRequireFullAccess());
templateContext.set("positive-trust", preferences.getPositiveTrust());
templateContext.set("negative-trust", preferences.getNegativeTrust());
templateContext.set("trust-comment", preferences.getTrustComment());
/*
- * Sone - OptionsPage.java - Copyright © 2010 David Roden
+ * Sone - SearchPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.collection.Converter;
-import net.pterodactylus.util.collection.Converters;
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
import net.pterodactylus.util.collection.Pagination;
import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
*/
public class SearchPage extends SoneTemplatePage {
+ /** The logger. */
+ private static final Logger logger = Logging.getLogger(SearchPage.class);
+
/**
* Creates a new search page.
*
Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR);
/* extract Sones and posts. */
- List<Sone> resultSones = Converters.convertList(sortedSoneHits, new HitConverter<Sone>());
- List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
+ List<Sone> resultSones = Mappers.mappedList(sortedSoneHits, new HitMapper<Sone>());
+ List<Post> resultPosts = Mappers.mappedList(sortedPostHits, new HitMapper<Post>());
/* pagination. */
Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
Set<Hit<T>> hits = new HashSet<Hit<T>>();
for (T object : objects) {
String objectString = stringGenerator.generateString(object);
- int score = calculateScore(phrases, objectString);
+ double score = calculateScore(phrases, objectString);
hits.add(new Hit<T>(object, score));
}
return hits;
* The expression to search
* @return The score of the expression
*/
- private int calculateScore(List<Phrase> phrases, String expression) {
- int optionalHits = 0;
- int requiredHits = 0;
+ private double calculateScore(List<Phrase> phrases, String expression) {
+ logger.log(Level.FINEST, "Calculating Score for “%s”…", expression);
+ double optionalHits = 0;
+ double requiredHits = 0;
int forbiddenHits = 0;
int requiredPhrases = 0;
for (Phrase phrase : phrases) {
}
int matches = 0;
int index = 0;
+ double score = 0;
while (index < expression.length()) {
int position = expression.toLowerCase().indexOf(phraseString, index);
if (position == -1) {
break;
}
+ score += Math.pow(1 - position / (double) expression.length(), 2);
index = position + phraseString.length();
+ logger.log(Level.FINEST, "Got hit at position %d.", position);
++matches;
}
+ logger.log(Level.FINEST, "Score: %f", score);
if (matches == 0) {
continue;
}
if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
- requiredHits += matches;
+ requiredHits += score;
}
if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
- optionalHits += matches;
+ optionalHits += score;
}
if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
forbiddenHits += matches;
@Override
public int compare(Hit<?> leftHit, Hit<?> rightHit) {
- return rightHit.getScore() - leftHit.getScore();
+ return (rightHit.getScore() < leftHit.getScore()) ? -1 : ((rightHit.getScore() > leftHit.getScore()) ? 1 : 0);
}
};
private final T object;
/** The score of the object. */
- private final int score;
+ private final double score;
/**
* Creates a new hit.
* @param score
* The score of the object
*/
- public Hit(T object, int score) {
+ public Hit(T object, double score) {
this.object = object;
this.score = score;
}
*
* @return The score of the object
*/
- public int getScore() {
+ public double getScore() {
return score;
}
* The type of the object to extract
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- public static class HitConverter<T> implements Converter<Hit<T>, T> {
+ public static class HitMapper<T> implements Mapper<Hit<T>, T> {
/**
* {@inheritDoc}
*/
@Override
- public T convert(Hit<T> input) {
+ public T map(Hit<T> input) {
return input.getObject();
}
/*
- * Freetalk - FreetalkTemplatePage.java - Copyright © 2010 David Roden
+ * Sone - SoneTemplatePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
import net.pterodactylus.sone.web.page.FreenetTemplatePage;
+import net.pterodactylus.sone.web.page.Page;
import net.pterodactylus.util.collection.ListBuilder;
import net.pterodactylus.util.collection.MapBuilder;
import net.pterodactylus.util.template.Template;
import freenet.support.api.HTTPRequest;
/**
- * Base page for the Freetalk web interface.
+ * Base page for the Sone web interface.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
private final boolean requireLogin;
/**
- * Creates a new template page for Freetalk that does not require the user
- * to be logged in.
+ * Creates a new template page for Sone that does not require the user to be
+ * logged in.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk that does not require the user
- * to be logged in.
+ * Creates a new template page for Sone that does not require the user to be
+ * logged in.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk.
+ * Creates a new template page for Sone.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk.
+ * Creates a new template page for Sone.
*
* @param path
* The path of the page
@Override
protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
- templateContext.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+ Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+ templateContext.set("currentSone", currentSone);
templateContext.set("localSones", webInterface.getCore().getLocalSones());
templateContext.set("request", request);
templateContext.set("currentVersion", SonePlugin.VERSION);
templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+ templateContext.set("notifications", ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone));
}
/**
* {@inheritDoc}
*/
@Override
+ protected boolean isFullAccessOnly() {
+ return webInterface.getCore().getPreferences().isRequireFullAccess();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
if (requiresLogin()) {
return getCurrentSone(toadletContext, false) != null;
}
/*
- * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - FollowSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnfollowSonePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - LockSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSonePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
String soneId = request.getHttpRequest().getParam("sone");
Sone sone = webInterface.getCore().getSone(soneId, false);
templateContext.set("sone", sone);
+ templateContext.set("soneId", soneId);
+ if (sone == null) {
+ return;
+ }
List<Post> sonePosts = sone.getPosts();
sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
Collections.sort(sonePosts, Post.TIME_COMPARATOR);
/*
- * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ * Sone - WebInterface.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
import net.pterodactylus.sone.template.HttpRequestAccessor;
import net.pterodactylus.sone.template.IdentityAccessor;
import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.NotificationManagerAccessor;
import net.pterodactylus.sone.template.ParserFilter;
import net.pterodactylus.sone.template.PostAccessor;
import net.pterodactylus.sone.template.ReplyAccessor;
import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetNotificationAjaxPage;
import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
import net.pterodactylus.sone.web.page.PageToadlet;
import net.pterodactylus.sone.web.page.PageToadletFactory;
+import net.pterodactylus.sone.web.page.RedirectPage;
import net.pterodactylus.sone.web.page.StaticPage;
import net.pterodactylus.sone.web.page.TemplatePage;
import net.pterodactylus.util.cache.Cache;
import net.pterodactylus.util.notify.NotificationManager;
import net.pterodactylus.util.notify.TemplateNotification;
import net.pterodactylus.util.template.CollectionSortFilter;
+import net.pterodactylus.util.template.ContainsFilter;
import net.pterodactylus.util.template.DateFilter;
import net.pterodactylus.util.template.FormatFilter;
import net.pterodactylus.util.template.HtmlFilter;
templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
- templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
templateContextFactory.addFilter("date", new DateFilter());
templateContextFactory.addFilter("format", new FormatFilter());
templateContextFactory.addFilter("sort", new CollectionSortFilter());
templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
+ templateContextFactory.addFilter("in", new ContainsFilter());
templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
templateContextFactory.addProvider(new ClassPathTemplateProvider());
templateContextFactory.addTemplateObject("formPassword", formPassword);
Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
import net.pterodactylus.sone.web.WebInterface;
import net.pterodactylus.util.json.JsonObject;
if ((text == null) || (text.trim().length() == 0)) {
return createErrorJsonObject("text-required");
}
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
Post newPost = webInterface.getCore().createPost(sender, recipient, text);
return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", (newPost.getRecipient() != null) ? newPost.getRecipient().getId() : null);
}
/*
- * Sone - DeleteReplysAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - DeleteReplyAjaxPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustAjaxPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
--- /dev/null
+/*
+ * Sone - GetNotificationAjaxPage.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The “get notification” AJAX handler returns a number of rendered
+ * notifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetNotificationAjaxPage extends JsonPage {
+
+ /**
+ * Creates a new “get notification” AJAX page.
+ *
+ * @param webInterface
+ * The Sone web interface
+ */
+ public GetNotificationAjaxPage(WebInterface webInterface) {
+ super("getNotification.ajax", webInterface);
+ }
+
+ //
+ // JSONPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean needsFormPassword() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean requiresLogin() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ protected JsonObject createJsonObject(Request request) {
+ String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
+ JsonObject jsonNotifications = new JsonObject();
+ Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
+ for (String notificationId : notificationIds) {
+ Notification notification = webInterface.getNotifications().getNotification(notificationId);
+ ListNotificationFilters.filterNotifications(new ArrayList<Notification>(), currentSone);
+ if ("new-post-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+ } else if ("new-reply-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+ }
+ if (notification == null) {
+ // TODO - show error
+ continue;
+ }
+ jsonNotifications.put(notificationId, createJsonNotification(request, notification));
+ }
+ return createSuccessJsonObject().put("notifications", jsonNotifications);
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Creates a JSON object from the given notification.
+ *
+ * @param request
+ * The request to load the session from
+ * @param notification
+ * The notification to create a JSON object
+ * @return The JSON object
+ */
+ private JsonObject createJsonNotification(Request request, Notification notification) {
+ JsonObject jsonNotification = new JsonObject();
+ jsonNotification.put("id", notification.getId());
+ StringWriter notificationWriter = new StringWriter();
+ try {
+ if (notification instanceof TemplateNotification) {
+ TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+ templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
+ templateContext.set("localSones", webInterface.getCore().getLocalSones());
+ templateContext.set("request", request);
+ templateContext.set("currentVersion", SonePlugin.VERSION);
+ templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+ templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+ templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+ templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+ templateContext.set("notification", notification);
+ ((TemplateNotification) notification).render(templateContext, notificationWriter);
+ } else {
+ notification.render(notificationWriter);
+ }
+ } catch (IOException ioe1) {
+ /* StringWriter never throws, ignore. */
+ }
+ jsonNotification.put("text", notificationWriter.toString());
+ jsonNotification.put("createdTime", notification.getCreatedTime());
+ jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
+ jsonNotification.put("dismissable", notification.isDismissable());
+ return jsonNotification;
+ }
+
+}
if (post == null) {
return createErrorJsonObject("invalid-post-id");
}
- return createSuccessJsonObject().put("post", createJsonPost(post, getCurrentSone(request.getToadletContext())));
+ return createSuccessJsonObject().put("post", createJsonPost(request, post, getCurrentSone(request.getToadletContext())));
}
/**
* Creates a JSON object from the given post. The JSON object will only
* contain the ID of the post, its time, and its rendered HTML code.
*
+ * @param request
+ * The request being processed
* @param post
* The post to create a JSON object from
* @param currentSone
* The currently logged in Sone (to store in the template)
* @return The JSON representation of the post
*/
- private JsonObject createJsonPost(Post post, Sone currentSone) {
+ private JsonObject createJsonPost(Request request, Post post, Sone currentSone) {
JsonObject jsonPost = new JsonObject();
jsonPost.put("id", post.getId());
jsonPost.put("sone", post.getSone().getId());
jsonPost.put("time", post.getTime());
StringWriter stringWriter = new StringWriter();
TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+ templateContext.set("request", request);
templateContext.set("post", post);
templateContext.set("currentSone", currentSone);
templateContext.set("localSones", webInterface.getCore().getLocalSones());
if ((reply == null) || (reply.getSone() == null)) {
return createErrorJsonObject("invalid-reply-id");
}
- return createSuccessJsonObject().put("reply", createJsonReply(reply, getCurrentSone(request.getToadletContext())));
+ return createSuccessJsonObject().put("reply", createJsonReply(request, reply, getCurrentSone(request.getToadletContext())));
}
/**
/**
* Creates a JSON representation of the given reply.
*
+ * @param request
+ * The request being processed
* @param reply
* The reply to convert
* @param currentSone
* The currently logged in Sone (to store in the template)
* @return The JSON representation of the reply
*/
- private JsonObject createJsonReply(Reply reply, Sone currentSone) {
+ private JsonObject createJsonReply(Request request, Reply reply, Sone currentSone) {
JsonObject jsonReply = new JsonObject();
jsonReply.put("id", reply.getId());
jsonReply.put("postId", reply.getPost().getId());
jsonReply.put("time", reply.getTime());
StringWriter stringWriter = new StringWriter();
TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+ templateContext.set("request", request);
templateContext.set("reply", reply);
templateContext.set("currentSone", currentSone);
try {
package net.pterodactylus.sone.web.ajax;
-import java.io.IOException;
-import java.io.StringWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import net.pterodactylus.util.json.JsonArray;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.TemplateContext;
/**
* The “get status” AJAX handler returns all information that is necessary to
protected JsonObject createJsonObject(Request request) {
final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
/* load Sones. */
- boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
+ boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "false"));
Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
if (loadAllSones) {
sones.addAll(webInterface.getCore().getSones());
+ } else {
+ String loadSoneIds = request.getHttpRequest().getParam("soneIds");
+ if (loadSoneIds.length() > 0) {
+ String[] soneIds = loadSoneIds.split(",");
+ for (String soneId : soneIds) {
+ /* just add it, we skip null further down. */
+ sones.add(webInterface.getCore().getSone(soneId, false));
+ }
+ }
}
JsonArray jsonSones = new JsonArray();
for (Sone sone : sones) {
/* load notifications. */
List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
- JsonArray jsonNotifications = new JsonArray();
+ JsonArray jsonNotificationInformations = new JsonArray();
for (Notification notification : notifications) {
- jsonNotifications.add(createJsonNotification(notification));
+ jsonNotificationInformations.add(createJsonNotificationInformation(notification));
}
/* load new posts. */
Set<Post> newPosts = webInterface.getNewPosts();
@Override
public boolean filterObject(Post post) {
- return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+ return ListNotificationFilters.isPostVisible(currentSone, post);
}
});
@Override
public boolean filterObject(Reply reply) {
- return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+ return ListNotificationFilters.isReplyVisible(currentSone, reply);
}
});
jsonReply.put("postSone", reply.getPost().getSone().getId());
jsonReplies.add(jsonReply);
}
- return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+ return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
}
/**
synchronized (dateFormat) {
jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
}
- jsonSone.put("age", (System.currentTimeMillis() - sone.getTime()) / 1000);
+ jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, System.currentTimeMillis() - sone.getTime()).getText());
return jsonSone;
}
/**
- * Creates a JSON object from the given notification.
+ * Creates a JSON object that only contains the ID and the last-updated time
+ * of the given notification.
*
+ * @see Notification#getId()
+ * @see Notification#getLastUpdatedTime()
* @param notification
- * The notification to create a JSON object
- * @return The JSON object
+ * The notification
+ * @return A JSON object containing the notification ID and last-updated
+ * time
*/
- private JsonObject createJsonNotification(Notification notification) {
+ private JsonObject createJsonNotificationInformation(Notification notification) {
JsonObject jsonNotification = new JsonObject();
jsonNotification.put("id", notification.getId());
- StringWriter notificationWriter = new StringWriter();
- try {
- if (notification instanceof TemplateNotification) {
- TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
- templateContext.set("notification", notification);
- ((TemplateNotification) notification).render(templateContext, notificationWriter);
- } else {
- notification.render(notificationWriter);
- }
- } catch (IOException ioe1) {
- /* StringWriter never throws, ignore. */
- }
- jsonNotification.put("text", notificationWriter.toString());
- jsonNotification.put("createdTime", notification.getCreatedTime());
jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
- jsonNotification.put("dismissable", notification.isDismissable());
return jsonNotification;
}
* @return The formatted age
*/
private Time getTime(long age) {
+ return getTime(webInterface, age);
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Returns the formatted relative time for a given age.
+ *
+ * @param webInterface
+ * The Sone web interface (for l10n access)
+ * @param age
+ * The age to format (in milliseconds)
+ * @return The formatted age
+ */
+ public static Time getTime(WebInterface webInterface, long age) {
String text;
long refresh;
if (age < 0) {
text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
refresh = Time.MINUTE;
} else if (age < 30 * Time.MINUTE) {
- text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+ text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) (Digits.round(age, Time.MINUTE) / Time.MINUTE)));
refresh = 1 * Time.MINUTE;
} else if (age < 45 * Time.MINUTE) {
text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
text = webInterface.getL10n().getString("View.Time.AnHourAgo");
refresh = Time.HOUR;
} else if (age < 21 * Time.HOUR) {
- text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+ text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) (Digits.round(age, Time.HOUR) / Time.HOUR)));
refresh = Time.HOUR;
} else if (age < 42 * Time.HOUR) {
text = webInterface.getL10n().getString("View.Time.ADayAgo");
refresh = Time.DAY;
} else if (age < 6 * Time.DAY) {
- text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+ text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) (Digits.round(age, Time.DAY) / Time.DAY)));
refresh = Time.DAY;
} else if (age < 11 * Time.DAY) {
text = webInterface.getL10n().getString("View.Time.AWeekAgo");
refresh = Time.DAY;
} else if (age < 4 * Time.WEEK) {
- text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+ text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) (Digits.round(age, Time.WEEK) / Time.WEEK)));
refresh = Time.DAY;
} else if (age < 6 * Time.WEEK) {
text = webInterface.getL10n().getString("View.Time.AMonthAgo");
refresh = Time.DAY;
} else if (age < 11 * Time.MONTH) {
- text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+ text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) (Digits.round(age, Time.MONTH) / Time.MONTH)));
refresh = Time.DAY;
} else if (age < 18 * Time.MONTH) {
text = webInterface.getL10n().getString("View.Time.AYearAgo");
refresh = Time.WEEK;
} else {
- text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+ text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) (Digits.round(age, Time.YEAR) / Time.YEAR)));
refresh = Time.WEEK;
}
return new Time(text, refresh);
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private static class Time {
+ public static class Time {
/** Number of milliseconds in a second. */
private static final long SECOND = 1000;
return refresh;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return text;
+ }
+
}
}
*/
@Override
public Response handleRequest(Request request) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
+ return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+ }
if (needsFormPassword()) {
String formPassword = request.getHttpRequest().getParam("formPassword");
if (!webInterface.getFormPassword().equals(formPassword)) {
- return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+ return new Response(403, "Forbidden", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
}
}
if (requiresLogin()) {
if (getCurrentSone(request.getToadletContext(), false) == null) {
- return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+ return new Response(403, "Forbidden", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
}
}
JsonObject jsonObject = createJsonObject(request);
/*
- * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkAjaxPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - LockSoneAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSoneAjaxPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustAjaxPage.java - Copyright © 2011 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * shortener - TemplatePage.java - Copyright © 2010 David Roden
+ * Sone - FreenetTemplatePage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
return new RedirectResponse(redirectTarget);
}
+ if (isFullAccessOnly() && !request.getToadletContext().isAllowedFullAccess()) {
+ return new Response(401, "Not authorized", "text/html", "Not authorized");
+ }
ToadletContext toadletContext = request.getToadletContext();
if (request.getMethod() == Method.POST) {
/* require form password. */
return Collections.emptyList();
}
+ /**
+ * Returns whether this page should only be allowed for requests from hosts
+ * with full access.
+ *
+ * @return {@code true} if this page should only be allowed for hosts with
+ * full access, {@code false} to allow this page for any host
+ */
+ protected boolean isFullAccessOnly() {
+ return false;
+ }
+
//
// INTERFACE LinkEnabledCallback
//
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
- return true;
+ return !isFullAccessOnly();
}
/**
/*
- * shortener - Page.java - Copyright © 2010 David Roden
+ * Sone - Page.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * shortener - PageToadlet.java - Copyright © 2010 David Roden
+ * Sone - PageToadlet.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
/*
- * shortener - PageToadletFactory.java - Copyright © 2010 David Roden
+ * Sone - PageToadletFactory.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
--- /dev/null
+/*
+ * Sone - RedirectPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.page;
+
+/**
+ * Page implementation that redirects the user to another URL.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RedirectPage implements Page {
+
+ /** The original path. */
+ private String originalPath;
+
+ /** The path to redirect the browser to. */
+ private String newPath;
+
+ /**
+ * Creates a new redirect page.
+ *
+ * @param originalPath
+ * The original path
+ * @param newPath
+ * The path to redirect the browser to
+ */
+ public RedirectPage(String originalPath, String newPath) {
+ this.originalPath = originalPath;
+ this.newPath = newPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPath() {
+ return originalPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response handleRequest(Request request) {
+ return new RedirectResponse(newPath);
+ }
+
+}
/*
- * shortener - CSSPage.java - Copyright © 2010 David Roden
+ * Sone - StaticPage.java - Copyright © 2010 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Page.Options.Page.Description=These options influence the runtime behaviour of the Sone plugin.
Page.Options.Section.SoneSpecificOptions.Title=Sone-specific Options
Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only available if you are {link}logged in{/link}.
-Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically.
+Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
+Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically. Note that this will only follow Sones that are discovered after you activate this option!
Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
+Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
Page.Options.Section.TrustOptions.Title=Trust Settings
Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
Page.Options.Section.RescueOptions.Title=Rescue Settings
-Page.Options.Option.SoneRescueMode.Description=Try to rescue your Sones at the next start of the Sone plugin. This will read your all your old Sones from Freenet and ignore any disappearing postings and replies. You have to unlock your local Sones after they have been restored and you have to manually disable the rescue mode once you are satisfied with what has been restored!
+Page.Options.Option.SoneRescueMode.Description1=Try to rescue your Sones at the next start of the Sone plugin. The Rescue Mode will start at the latest known edition and will try to download all editions sequentially backwards, merging all discovered posts and replies together, until it is stopped or it has reached the first edition.
+Page.Options.Option.SoneRescueMode.Description2=When using the Rescue Mode because Sone lost its configuration it usually suffices to let the Rescue Mode only run for a short time; use a second tab to control how many posts of your Sone are visible again. As soon as the last valid edition is loaded you can then deactivate the Rescue Mode.
+Page.Options.Option.SoneRescueMode.Description3=Note that when you use the Rescue Mode posts that you have deleted after they have been inserted will have to be deleted again. Unfortunately this is an unavoidable side effect of the Rescue Mode.
Page.Options.Section.Cleaning.Title=Clean Up
Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
+Page.Options.Warnings.ValueNotChanged=This option was not changed because value you specified was not valid.
Page.Options.Button.Save=Save
Page.Login.Title=Login - Sone
Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
Page.ViewSone.Profile.Title=Profile
Page.ViewSone.Profile.Label.Name=Name
-Page.ViewSone.Replies.Title=Replies to Posts
+Page.ViewSone.Profile.Name.WoTLink=Web of trust profile
+Page.ViewSone.Replies.Title=Posts {sone} has replied to
Page.ViewPost.Title=View Post - Sone
Page.ViewPost.Page.Title=View Post by {sone}
View.Sone.Status.Inserting=This Sone is currently being inserted.
View.Post.UnknownAuthor=(unknown)
+View.Post.Permalink=link post
+View.Post.PermalinkAuthor=link author
View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
View.Post.DeleteLink=Delete
View.Time.ADayAgo=about a day ago
View.Time.XDaysAgo=${day} days ago
View.Time.AWeekAgo=about a week ago
-View.Time.XWeeksAgo=${week} week ago
+View.Time.XWeeksAgo=${week} weeks ago
View.Time.AMonthAgo=about a month ago
View.Time.XMonthsAgo=${month} months ago
View.Time.AYearAgo=about a year ago
content: '★ ';
}
+#sone a.in-page-link:before {
+ content: '↓ ';
+}
+
#sone a img {
border: none;
}
+#sone #main.offline {
+ opacity: 0.5;
+}
+
+#sone #offline-marker {
+ display: none;
+ position: fixed;
+ top: 2em;
+ right: 2em;
+ width: 128px;
+ height: 128px;
+ background-image: url("../images/sone-offline.png");
+}
+
#sone #notification-area {
margin-top: 1em;
}
display: inline;
}
+#sone .permalink {
+ display: inline;
+}
+
#sone .post .bookmarks {
display: inline;
color: rgb(28, 131, 191);
font-weight: bold;
color: red;
}
+
+#sone .warning {
+ color: red;
+ font-style: italic;
+}
/* Sone JavaScript functions. */
-/* jQuery overrides. */
-oldGetJson = jQuery.prototype.getJSON;
-jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
- if (typeof errorCallback == "undefined") {
- return oldGetJson(url, data, successCallback);
- }
- if (jQuery.isFunction(data)) {
- errorCallback = successCallback;
- successCallback = data;
- data = null;
- }
- return jQuery.ajax({
- data: data,
- error: errorCallback,
- success: successCallback,
- url: url
- });
-}
-
-function isOnline() {
- return $("#sone").hasClass("online");
+function ajaxGet(url, data, successCallback, errorCallback) {
+ (function(url, data, successCallback, errorCallback) {
+ $.ajax({"type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+ ajaxSuccess();
+ if (typeof successCallback != "undefined") {
+ successCallback(data, textStatus);
+ }
+ }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+ if (typeof errorCallback != "undefined") {
+ errorCallback();
+ } else {
+ ajaxError();
+ }
+ }});
+ })(url, data, successCallback, errorCallback);
}
function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
inputField.val(defaultText);
}
}).hide().data("inputField", $(this)).val($(this).val());
- $(this).after(textarea);
+ $(this).data("textarea", textarea).after(textarea);
(function(inputField, textarea) {
inputField.focus(function() {
$(this).hide().attr("disabled", "disabled");
* @param element
* The element to add a “comment” link to
*/
-function addCommentLink(postId, element, insertAfterThisElement) {
+function addCommentLink(postId, author, element, insertAfterThisElement) {
if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) {
return;
}
- commentElement = (function(postId) {
+ commentElement = (function(postId, author) {
separator = $("<span> · </span>").addClass("separator");
var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
replyElement = $("#sone .post#" + postId + " .create-reply");
replyElement.removeClass("light");
});
})(replyElement);
- replyElement.find("input.reply-input").focus();
+ textArea = replyElement.find("input.reply-input").focus().data("textarea");
+ textArea.val(textArea.val() + "@sone://" + author + " ");
});
return commentElement;
- })(postId);
+ })(postId, author);
$(insertAfterThisElement).after(commentElement.clone(true));
$(insertAfterThisElement).after(separator);
}
callback(translations[key]);
return;
}
- $.getJSON("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+ ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
if ((data != null) && data.success) {
translations[key] = data.value;
callback(data.value);
}
- }, function(xmlHttpRequest, textStatus, error) {
- /* ignore error. */
});
}
* @param lastUpdated
* The date and time of the last update (formatted for display)
*/
-function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
+function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
$("#sone .sone." + filterSoneId(soneId)).
toggleClass("unknown", status == "unknown").
toggleClass("idle", status == "idle").
$("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
$("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
if (lastUpdated != null) {
- $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+ $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
} else {
getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
$("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
*/
function enhanceDeletePostButton(button, postId, text) {
enhanceDeleteButton(button, text, function() {
- $.getJSON("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if (data == null) {
return;
}
*/
function enhanceDeleteReplyButton(button, replyId, text) {
enhanceDeleteButton(button, text, function() {
- $.getJSON("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
+ ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
if (data == null) {
return;
}
*/
function getSone(soneId) {
return $("#sone .sone").filter(function(index) {
- return $(".id").text() == soneId;
+ return $(".id", this).text() == soneId;
});
}
return $(notificationElement).attr("id");
}
+/**
+ * Returns the time the notification was last updated.
+ *
+ * @param notificationElement
+ * The notification element
+ * @returns The last update time of the notification
+ */
+function getNotificationLastUpdatedTime(notificationElement) {
+ return $(notificationElement).attr("lastUpdatedTime");
+}
+
function likePost(postId) {
- $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function unlikePost(postId) {
- $.getJSON("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function updatePostLikes(postId) {
- $.getJSON("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+ ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
if ((data != null) && data.success) {
- $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
+ $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
$("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
$("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
}
}
function likeReply(replyId) {
- $.getJSON("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function unlikeReply(replyId) {
- $.getJSON("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
* The ID of the Sone to trust
*/
function trustSone(soneId) {
- $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
* The ID of the Sone to distrust
*/
function distrustSone(soneId) {
- $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
* The ID of the Sone to untrust
*/
function untrustSone(soneId) {
- $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
*/
function bookmarkPost(postId) {
(function(postId) {
- $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
if ((data != null) && data.success) {
getPost(postId).find(".bookmark").toggleClass("hidden", true);
getPost(postId).find(".unbookmark").toggleClass("hidden", false);
* The ID of the post to unbookmark
*/
function unbookmarkPost(postId) {
- $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
if ((data != null) && data.success) {
getPost(postId).find(".bookmark").toggleClass("hidden", false);
getPost(postId).find(".unbookmark").toggleClass("hidden", true);
}
function updateReplyLikes(replyId) {
- $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+ ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
if ((data != null) && data.success) {
- $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
+ $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
$("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
$("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
}
* parameters: success, error, replyId)
*/
function postReply(sender, postId, text, callbackFunction) {
- $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+ ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
if (data == null) {
/* TODO - show error */
return;
*/
$(".follow", soneElement).submit(function() {
var followElement = this;
- $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+ ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
$(followElement).addClass("hidden");
$(followElement).parent().find(".unfollow").removeClass("hidden");
});
});
$(".unfollow", soneElement).submit(function() {
var unfollowElement = this;
- $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+ ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
$(unfollowElement).addClass("hidden");
$(unfollowElement).parent().find(".follow").removeClass("hidden");
});
});
$(".lock", soneElement).submit(function() {
var lockElement = this;
- $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+ ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
$(lockElement).addClass("hidden");
$(lockElement).parent().find(".unlock").removeClass("hidden");
});
});
$(".unlock", soneElement).submit(function() {
var unlockElement = this;
- $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+ ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
$(unlockElement).addClass("hidden");
$(unlockElement).parent().find(".lock").removeClass("hidden");
});
/* mark Sone as known when clicking it. */
$(soneElement).click(function() {
- markSoneAsKnown(soneElement);
+ markSoneAsKnown(this);
});
}
});
/* add “comment” link. */
- addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
+ addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
/* process all replies. */
replyIds = [];
});
});
})(replyElement);
- addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
+ addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
/* convert “show source” link into javascript function. */
$(replyElement).find(".show-reply-source").each(function() {
notification.find(".text").addClass("hidden");
}
notification.find("form.mark-as-read button").click(function() {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
});
notification.find("a[class^='link-']").each(function() {
linkElement = $(this);
if (linkElement.is("[href^='viewPost']")) {
id = linkElement.attr("class").substr(5);
if (hasPost(id)) {
- linkElement.attr("href", "#post-" + id);
+ linkElement.attr("href", "#post-" + id).addClass("in-page-link");
}
}
});
notification.find("form.dismiss button").click(function() {
- $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+ ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
/* dismiss in case of error, too. */
notification.slideUp();
}, function(xmlHttpRequest, textStatus, error) {
if (getNotificationId(oldNotification) != "new-sone-notification") {
return;
}
- oldIds = getElementIds(oldNotification, ".sone-id");
- newIds = getElementIds(newNotification, ".sone-id");
+ oldIds = getElementIds(oldNotification, ".new-sone-id");
+ newIds = getElementIds(newNotification, ".new-sone-id");
$.each(oldIds, function(index, value) {
if ($.inArray(value, newIds) == -1) {
markSoneAsKnown(getSone(value), true);
}
function getStatus() {
- $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
+ ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
if ((data != null) && data.success) {
/* process Sone information. */
$.each(data.sones, function(index, value) {
- updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
+ updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated, value.lastUpdatedText);
});
/* search for removed notifications. */
$("#sone #notification-area .notification").each(function() {
});
if (!foundNotification) {
if (notificationId == "new-sone-notification") {
- $(".sone-id", this).each(function(index, element) {
+ $(".new-sone-id", this).each(function(index, element) {
soneId = $(this).text();
markSoneAsKnown(getSone(soneId), true);
});
}
});
/* process notifications. */
+ notificationIds = [];
$.each(data.notifications, function(index, value) {
oldNotification = getNotification(value.id);
- notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
- if (oldNotification.length != 0) {
- if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
- opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
- notification.find(".short-text").toggleClass("hidden", opened);
- notification.find(".text").toggleClass("hidden", !opened);
- }
- checkForRemovedSones(oldNotification, notification);
- checkForRemovedPosts(oldNotification, notification);
- checkForRemovedReplies(oldNotification, notification);
- oldNotification.replaceWith(notification.show());
- } else {
- $("#sone #notification-area").append(notification);
- notification.slideDown();
- setActivity();
+ if ((oldNotification.length == 0) || (value.lastUpdatedTime > getNotificationLastUpdatedTime(oldNotification))) {
+ notificationIds.push(value.id);
}
});
+ if (notificationIds.length > 0) {
+ loadNotifications(notificationIds);
+ }
/* process new posts. */
$.each(data.newPosts, function(index, value) {
loadNewPost(value.id, value.sone, value.recipient, value.time);
/* data.success was false, wait 30 seconds. */
setTimeout(getStatus, 30000);
}
- }, function(xmlHttpRequest, textStatus, error) {
- /* something really bad happend, wait a minute. */
- setTimeout(getStatus, 60000);
- })
+ }, function() {
+ statusRequestQueued = false;
+ ajaxError();
+ });
+}
+
+/**
+ * Requests multiple notifications from Sone and displays them.
+ *
+ * @param notificationIds
+ * Array of IDs of the notifications to load
+ */
+function loadNotifications(notificationIds) {
+ ajaxGet("getNotification.ajax", {"notifications": notificationIds.join(",")}, function(data, textStatus) {
+ if (!data || !data.success) {
+ // TODO - show error
+ return;
+ }
+ $.each(data.notifications, function(index, value) {
+ oldNotification = getNotification(value.id);
+ notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+ if (oldNotification.length != 0) {
+ if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+ opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+ notification.find(".short-text").toggleClass("hidden", opened);
+ notification.find(".text").toggleClass("hidden", !opened);
+ }
+ checkForRemovedSones(oldNotification, notification);
+ checkForRemovedPosts(oldNotification, notification);
+ checkForRemovedReplies(oldNotification, notification);
+ oldNotification.replaceWith(notification.show());
+ } else {
+ $("#sone #notification-area").append(notification);
+ notification.slideDown();
+ setActivity();
+ }
+ });
+ });
}
/**
}
/**
+ * Returns the current page of the selected pagination. If no pagination can be
+ * found with the given selector, {@code 1} is returned.
+ *
+ * @param paginationSelector
+ * The pagination selector
+ * @returns The current page of the pagination
+ */
+function getPage(paginationSelector) {
+ pagination = $(paginationSelector);
+ if (pagination.length > 0) {
+ return $(".current-page", paginationSelector).text();
+ }
+ return 1;
+}
+
+/**
* Returns whether the current page is a “view Sone” page.
*
* @returns {Boolean} <code>true</code> if the current page is a “view Sone”
if (hasPost(postId)) {
return;
}
- if (!isIndexPage()) {
+ if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
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) {
+ ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
if ((data != null) && data.success) {
if (hasPost(data.post.id)) {
return;
}
- if (!isIndexPage() && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
+ if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
return;
}
var firstOlderPost = null;
if (!hasPost(postId)) {
return;
}
- $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+ ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
/* find post. */
if ((data != null) && data.success) {
if (hasReply(data.reply.id)) {
* request
*/
function markSoneAsKnown(soneElement, skipRequest) {
- if ($(".new", soneElement).length > 0) {
- if ((typeof skipRequest != "undefined") && !skipRequest) {
- $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
- $(soneElement).removeClass("new");
- });
+ if ($(soneElement).is(".new")) {
+ $(soneElement).removeClass("new");
+ if ((typeof skipRequest == "undefined") || !skipRequest) {
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)});
}
}
}
$(postElement).removeClass("new");
$(".click-to-show", postElement).removeClass("new");
if ((typeof skipRequest == "undefined") || !skipRequest) {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
}
})(postElement);
}
(function(replyElement) {
$(replyElement).removeClass("new");
if ((typeof skipRequest == "undefined") || !skipRequest) {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
}
})(replyElement);
}
* Comma-separated post IDs
*/
function updatePostTimes(postIds) {
- $.getJSON("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
+ ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
if ((data != null) && data.success) {
$.each(data.postTimes, function(index, value) {
updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
* Comma-separated post IDs
*/
function updateReplyTimes(replyIds) {
- $.getJSON("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
+ ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
if ((data != null) && data.success) {
$.each(data.replyTimes, function(index, value) {
updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
* <code>true</code> if the notification can be dismissed by the
* user
*/
-function createNotification(id, text, dismissable) {
- notification = $("<div></div>").addClass("notification").attr("id", id);
+function createNotification(id, lastUpdatedTime, text, dismissable) {
+ notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
if (dismissable) {
- dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
+ dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
dismissForm.find("input[name=notification]").val(id);
notification.append(dismissForm);
}
* The ID of the field to delete
*/
function deleteProfileField(fieldId) {
- $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+ ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
if (data && data.success) {
$("#sone .profile-field#" + data.field.id).slideUp();
}
* Called when the renaming was successful
*/
function editProfileField(fieldId, newName, successFunction) {
- $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+ ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
if (data && data.success) {
successFunction();
}
* Function to call on success
*/
function moveProfileField(fieldId, direction, successFunction) {
- $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+ ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
if (data && data.success) {
successFunction();
}
moveProfileField(fieldId, "down", successFunction);
}
+var statusRequestQueued = true;
+
+/**
+ * Sets the status of the web interface as offline.
+ */
+function ajaxError() {
+ online = false;
+ toggleOfflineMarker(true);
+ if (!statusRequestQueued) {
+ setTimeout(getStatus, 5000);
+ statusRequestQueued = true;
+ }
+}
+
+/**
+ * Sets the status of the web interface as online.
+ */
+function ajaxSuccess() {
+ online = true;
+ toggleOfflineMarker(false);
+}
+
+/**
+ * Shows or hides the offline marker.
+ *
+ * @param visible
+ * {@code true} to display the offline marker, {@code false} to hide
+ * it
+ */
+function toggleOfflineMarker(visible) {
+ /* jQuery documentation says toggle() works the other way around?! */
+ $("#sone #offline-marker").toggle(visible);
+ if (visible) {
+ $("#sone #main").addClass("offline");
+ } else {
+ $("#sone #main").removeClass("offline");
+ }
+}
+
//
// EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
//
var focus = true;
+var online = true;
$(document).ready(function() {
}
sender = $(this).find(":input[name=sender]").val();
text = $(this).find(":input[name=text]:enabled").val();
- $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+ ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
button.removeAttr("disabled");
});
$(this).find(":input[name=sender]").val(getCurrentSoneId());
$("#sone #post-message").submit(function() {
sender = $(this).find(":input[name=sender]").val();
text = $(this).find(":input[name=text]:enabled").val();
- $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
+ ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
$(this).find(":input[name=sender]").val(getCurrentSoneId());
$(this).find(":input[name=text]:enabled").val("").blur();
$(this).find(".sender").hide();
ajaxifyNotification($(this));
});
+ /* disable all permalinks. */
+ $(".permalink").click(function() {
+ return false;
+ });
+
/* activate status polling. */
setTimeout(getStatus, 5000);
resetActivity();
}).blur(function() {
focus = false;
- })
+ });
});
-<div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
+<div id="sone">
<div id="formPassword"><% formPassword|html></div>
<div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
<script src="javascript/jquery.url.js" language="javascript"></script>
<script src="javascript/sone.js" language="javascript"></script>
+ <div id="offline-marker"></div>
+
<div id="main">
<div id="notification-area">
<button type="submit"><%= Notification.Button.Dismiss|l10n|html></button>
</form>
- <%foreach webInterface.notifications.all notification>
- <div class="notification" id="<% notification.id|html>">
+ <%foreach notifications notification>
+ <div class="notification" id="<% notification.id|html>" lastUpdatedTime="<%notification.lastUpdatedTime|html>">
<%if notification.dismissable>
<form class="dismiss" action="dismissNotification.html" method="post">
<input type="hidden" name="formPassword" value="<% formPassword|html>" />
<%if pagination.necessary>
- <div class="navigation">
+ <div class="navigation <%paginationName|html>">
<div class="first"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter value=0>">«</a><%else><span>«</span><%/if></div>
<div class="previous"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter key=pagination.previousPage>">‹</a><%else><span>‹</span><%/if></div>
<div class="current-page"><% pagination.pageNumber></div>
</div>
<span class='separator'>·</span>
<div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-post"><a href="post://<%post.id|html>">[<%= View.Post.Permalink|l10n|html>]</a></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-author"><a href="sone://<%post.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
<%if ! originalText|match key=parsedText>
<span class='separator'>·</span>
<div class="show-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
</div>
<div class="reply-status-line status-line">
<div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-author"><a href="sone://<%reply.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
<%if ! originalText|match key=parsedText>
<span class='separator'>·</span>
<div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
<div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
<div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
<div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
- <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss"></span></div>
+ <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
<div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
<div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
<div class="hidden"><% sone.blacklisted></div>
<div id="posts">
<%= page|store key=pageParameter>
- <%include include/pagination.html>
+ <%include include/pagination.html paginationName==pagination-index>
<%foreach pagination.items post>
<%include include/viewPost.html>
<%foreachelse>
<h1>Sone “<% currentSone.name|html>”</h1>
<p>
This page should not be viewed directly; it is merely a reminder
- that you should install the <i>Sone</i> plugin.
+ that you should install the <a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/<% currentEdition>/">Sone</a> plugin.
</p>
</body>
</html>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="post" />
+ <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewPost.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="post" />
- <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%= Notification.NewPost.Text|l10n|html>
<%foreach posts post>
<div class="hidden post-id"><%post.id|html></div>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="reply" />
+ <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewReply.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="reply" />
- <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
<%= Notification.NewReply.Text|l10n|html>
<%foreach replies postGroup|replyGroup>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="sone" />
+ <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewSone.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="sone" />
- <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%= Notification.NewSone.Text|l10n|html>
<%foreach sones sone>
- <div class="hidden sone-id"><% sone.id|html></div>
+ <div class="hidden new-sone-id"><% sone.id|html></div>
<a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
<%/foreach>
</div>
<%ifnull currentSone>
<p><%= Page.Options.Section.SoneSpecificOptions.NotLoggedIn|l10n|html|replace needle="{link}" replacement='<a href="login.html">'|replace needle="{/link}" replacement='</a>'></p>
+ <%else>
+ <p><%= Page.Options.Section.SoneSpecificOptions.LoggedIn|l10n|html></p>
<%/if>
<p>
<h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
+ <%if =insertion-delay|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
<p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
+ <%if =posts-per-page|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
+ <p>
+ <input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
+ <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
+ </p>
+
<h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
+ <%if =positive-trust|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
<p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
+ <%if =negative-trust|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
<p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
<h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
- <p><%= Page.Options.Option.SoneRescueMode.Description|l10n|html></p>
+ <p><%= Page.Options.Option.SoneRescueMode.Description1|l10n|html></p>
+ <p><%= Page.Options.Option.SoneRescueMode.Description2|l10n|html></p>
+ <p><%= Page.Options.Option.SoneRescueMode.Description3|l10n|html></p>
<p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
<h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>
<h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
- <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=sone.id|html></p>
+ <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=soneId|html></p>
<%elseifnull sone.name>
<div class="profile-field">
<div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
- <div class="value"><a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+ <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
</div>
<%foreach sone.profile.fields field>
<%foreach repliedPosts post>
<%first>
- <h2><%= Page.ViewSone.Replies.Title|l10n|html></h2>
+ <h2><%= Page.ViewSone.Replies.Title|l10n|html|replace needle="{sone}" replacementKey=sone.niceName></h2>
<div id="replied-posts">
<%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
<%/first>
TemplateContextFactory templateContextFactory = new TemplateContextFactory();
templateContextFactory.addFilter("html", new HtmlFilter());
FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
- FreenetLinkParserContext context = new FreenetLinkParserContext(null);
+ FreenetLinkParserContext context = new FreenetLinkParserContext(null, null);
Part part;
part = parser.parse(context, new StringReader("Text."));