🔥 Remove all trust/distrust/untrust pages
[Sone.git] / src / main / java / net / pterodactylus / sone / web / WebInterface.java
1 /*
2  * Sone - WebInterface.java - Copyright Â© 2010–2019 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.web;
19
20 import static com.google.common.collect.FluentIterable.from;
21 import static java.util.logging.Logger.getLogger;
22
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.TimeZone;
30 import java.util.UUID;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35 import java.util.logging.Logger;
36 import javax.annotation.Nonnull;
37 import javax.annotation.Nullable;
38
39 import net.pterodactylus.sone.core.Core;
40 import net.pterodactylus.sone.core.ElementLoader;
41 import net.pterodactylus.sone.core.event.*;
42 import net.pterodactylus.sone.data.Image;
43 import net.pterodactylus.sone.data.Post;
44 import net.pterodactylus.sone.data.PostReply;
45 import net.pterodactylus.sone.data.Sone;
46 import net.pterodactylus.sone.freenet.L10nFilter;
47 import net.pterodactylus.sone.freenet.Translation;
48 import net.pterodactylus.sone.main.Loaders;
49 import net.pterodactylus.sone.main.PluginHomepage;
50 import net.pterodactylus.sone.main.PluginVersion;
51 import net.pterodactylus.sone.main.PluginYear;
52 import net.pterodactylus.sone.main.SonePlugin;
53 import net.pterodactylus.sone.notify.ListNotification;
54 import net.pterodactylus.sone.notify.ListNotificationFilter;
55 import net.pterodactylus.sone.notify.PostVisibilityFilter;
56 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
57 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
58 import net.pterodactylus.sone.template.ParserFilter;
59 import net.pterodactylus.sone.template.RenderFilter;
60 import net.pterodactylus.sone.template.ShortenFilter;
61 import net.pterodactylus.sone.text.Part;
62 import net.pterodactylus.sone.text.SonePart;
63 import net.pterodactylus.sone.text.SoneTextParser;
64 import net.pterodactylus.sone.text.TimeTextConverter;
65 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
66 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
67 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
68 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
69 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
70 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
71 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
72 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
73 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
74 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
75 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
76 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
77 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
78 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
79 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
80 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
81 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
82 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
83 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
84 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
85 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
86 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
87 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
88 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
89 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
90 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
91 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
92 import net.pterodactylus.sone.web.page.FreenetRequest;
93 import net.pterodactylus.sone.web.page.TemplateRenderer;
94 import net.pterodactylus.sone.web.pages.*;
95 import net.pterodactylus.util.notify.Notification;
96 import net.pterodactylus.util.notify.NotificationManager;
97 import net.pterodactylus.util.notify.TemplateNotification;
98 import net.pterodactylus.util.template.Template;
99 import net.pterodactylus.util.template.TemplateContextFactory;
100 import net.pterodactylus.util.web.RedirectPage;
101 import net.pterodactylus.util.web.TemplatePage;
102
103 import freenet.clients.http.SessionManager;
104 import freenet.clients.http.SessionManager.Session;
105 import freenet.clients.http.ToadletContext;
106
107 import com.codahale.metrics.*;
108 import com.google.common.base.Optional;
109 import com.google.common.collect.Collections2;
110 import com.google.common.collect.ImmutableSet;
111 import com.google.common.eventbus.Subscribe;
112 import com.google.inject.Inject;
113
114 /**
115  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
116  * references to l10n helpers.
117  */
118 public class WebInterface implements SessionProvider {
119
120         /** The logger. */
121         private static final Logger logger = getLogger(WebInterface.class.getName());
122
123         /** The loaders for templates, pages, and classpath providers. */
124         private final Loaders loaders;
125
126         /** The notification manager. */
127         private final NotificationManager notificationManager = new NotificationManager();
128
129         /** The Sone plugin. */
130         private final SonePlugin sonePlugin;
131
132         /** The form password. */
133         private final String formPassword;
134
135         /** The template context factory. */
136         private final TemplateContextFactory templateContextFactory;
137         private final TemplateRenderer templateRenderer;
138
139         /** The Sone text parser. */
140         private final SoneTextParser soneTextParser;
141
142         /** The parser filter. */
143         private final ParserFilter parserFilter;
144         private final ShortenFilter shortenFilter;
145         private final RenderFilter renderFilter;
146
147         private final ListNotificationFilter listNotificationFilter;
148         private final PostVisibilityFilter postVisibilityFilter;
149         private final ReplyVisibilityFilter replyVisibilityFilter;
150
151         private final ElementLoader elementLoader;
152         private final LinkedElementRenderFilter linkedElementRenderFilter;
153         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
154         private final L10nFilter l10nFilter;
155
156         private final PageToadletRegistry pageToadletRegistry;
157         private final MetricRegistry metricRegistry;
158         private final Translation translation;
159
160         /** The â€śnew Sone” notification. */
161         private final ListNotification<Sone> newSoneNotification;
162
163         /** The â€śnew post” notification. */
164         private final ListNotification<Post> newPostNotification;
165
166         /** The â€śnew reply” notification. */
167         private final ListNotification<PostReply> newReplyNotification;
168
169         /** The invisible â€ślocal post” notification. */
170         private final ListNotification<Post> localPostNotification;
171
172         /** The invisible â€ślocal reply” notification. */
173         private final ListNotification<PostReply> localReplyNotification;
174
175         /** The â€śyou have been mentioned” notification. */
176         private final ListNotification<Post> mentionNotification;
177
178         /** Notifications for sone inserts. */
179         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
180
181         /** Sone locked notification ticker objects. */
182         private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
183
184         /** The â€śSone locked” notification. */
185         private final ListNotification<Sone> lockedSonesNotification;
186
187         /** The â€śnew version” notification. */
188         private final TemplateNotification newVersionNotification;
189
190         /** The â€śinserting images” notification. */
191         private final ListNotification<Image> insertingImagesNotification;
192
193         /** The â€śinserted images” notification. */
194         private final ListNotification<Image> insertedImagesNotification;
195
196         /** The â€śimage insert failed” notification. */
197         private final ListNotification<Image> imageInsertFailedNotification;
198
199         /** Scheduled executor for time-based notifications. */
200         private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
201
202         @Inject
203         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
204                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
205                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
206                         TemplateRenderer templateRenderer,
207                         ParserFilter parserFilter, ShortenFilter shortenFilter,
208                         RenderFilter renderFilter,
209                         LinkedElementRenderFilter linkedElementRenderFilter,
210                         PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter) {
211                 this.sonePlugin = sonePlugin;
212                 this.loaders = loaders;
213                 this.listNotificationFilter = listNotificationFilter;
214                 this.postVisibilityFilter = postVisibilityFilter;
215                 this.replyVisibilityFilter = replyVisibilityFilter;
216                 this.elementLoader = elementLoader;
217                 this.templateRenderer = templateRenderer;
218                 this.parserFilter = parserFilter;
219                 this.shortenFilter = shortenFilter;
220                 this.renderFilter = renderFilter;
221                 this.linkedElementRenderFilter = linkedElementRenderFilter;
222                 this.pageToadletRegistry = pageToadletRegistry;
223                 this.metricRegistry = metricRegistry;
224                 this.l10nFilter = l10nFilter;
225                 this.translation = translation;
226                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
227                 soneTextParser = new SoneTextParser(getCore(), getCore());
228
229                 this.templateContextFactory = templateContextFactory;
230                 templateContextFactory.addTemplateObject("webInterface", this);
231                 templateContextFactory.addTemplateObject("formPassword", formPassword);
232
233                 /* create notifications. */
234                 Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
235                 newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
236
237                 Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
238                 newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
239
240                 Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
241                 localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
242
243                 Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
244                 newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
245
246                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
247                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
248
249                 Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
250                 mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
251
252                 Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
253                 lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
254
255                 Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
256                 newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
257
258                 Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
259                 insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
260
261                 Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
262                 insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
263
264                 Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
265                 imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
266         }
267
268         //
269         // ACCESSORS
270         //
271
272         /**
273          * Returns the Sone core used by the Sone plugin.
274          *
275          * @return The Sone core
276          */
277         @Nonnull
278         public Core getCore() {
279                 return sonePlugin.core();
280         }
281
282         /**
283          * Returns the template context factory of the web interface.
284          *
285          * @return The template context factory
286          */
287         public TemplateContextFactory getTemplateContextFactory() {
288                 return templateContextFactory;
289         }
290
291         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
292                 return getSessionManager().useSession(toadletContenxt);
293         }
294
295         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
296                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
297                 if (session == null) {
298                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
299                 }
300                 return session;
301         }
302
303         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
304                 Collection<Sone> localSones = getCore().getLocalSones();
305                 if (localSones.size() == 1) {
306                         return localSones.iterator().next();
307                 }
308                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
309         }
310
311         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
312                 Collection<Sone> localSones = getCore().getLocalSones();
313                 if (localSones.size() == 1) {
314                         return localSones.iterator().next();
315                 }
316                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
317         }
318
319         /**
320          * Returns the currently logged in Sone.
321          *
322          * @param session
323          *            The session
324          * @return The currently logged in Sone, or {@code null} if no Sone is
325          *         currently logged in
326          */
327         private Sone getCurrentSone(Session session) {
328                 if (session == null) {
329                         return null;
330                 }
331                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
332                 if (soneId == null) {
333                         return null;
334                 }
335                 return getCore().getLocalSone(soneId);
336         }
337
338         @Override
339         @Nullable
340         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
341                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
342         }
343
344         /**
345          * Sets the currently logged in Sone.
346          *
347          * @param toadletContext
348          *            The toadlet context
349          * @param sone
350          *            The Sone to set as currently logged in
351          */
352         @Override
353         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
354                 Session session = getOrCreateCurrentSession(toadletContext);
355                 if (sone == null) {
356                         session.removeAttribute("Sone.CurrentSone");
357                 } else {
358                         session.setAttribute("Sone.CurrentSone", sone.getId());
359                 }
360         }
361
362         /**
363          * Returns the notification manager.
364          *
365          * @return The notification manager
366          */
367         public NotificationManager getNotifications() {
368                 return notificationManager;
369         }
370
371         @Nonnull
372         public Optional<Notification> getNotification(@Nonnull String notificationId) {
373                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
374         }
375
376         @Nonnull
377         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
378                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
379         }
380
381         public Translation getTranslation() {
382                 return translation;
383         }
384
385         /**
386          * Returns the session manager of the node.
387          *
388          * @return The node’s session manager
389          */
390         public SessionManager getSessionManager() {
391                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
392         }
393
394         /**
395          * Returns the node’s form password.
396          *
397          * @return The form password
398          */
399         public String getFormPassword() {
400                 return formPassword;
401         }
402
403         /**
404          * Returns the posts that have been announced as new in the
405          * {@link #newPostNotification}.
406          *
407          * @return The new posts
408          */
409         public Set<Post> getNewPosts() {
410                 return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
411         }
412
413         @Nonnull
414         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
415                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
416                                 .addAll(newPostNotification.getElements())
417                                 .addAll(localPostNotification.getElements())
418                                 .build();
419                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
420         }
421
422         /**
423          * Returns the replies that have been announced as new in the
424          * {@link #newReplyNotification}.
425          *
426          * @return The new replies
427          */
428         public Set<PostReply> getNewReplies() {
429                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
430         }
431
432         @Nonnull
433         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
434                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
435                                 .addAll(newReplyNotification.getElements())
436                                 .addAll(localReplyNotification.getElements())
437                                 .build();
438                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
439         }
440
441         /**
442          * Sets whether the current start of the plugin is the first start. It is
443          * considered a first start if the configuration file does not exist.
444          *
445          * @param firstStart
446          *            {@code true} if no configuration file existed when Sone was
447          *            loaded, {@code false} otherwise
448          */
449         public void setFirstStart(boolean firstStart) {
450                 if (firstStart) {
451                         Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
452                         Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
453                         notificationManager.addNotification(firstStartNotification);
454                 }
455         }
456
457         /**
458          * Sets whether Sone was started with a fresh configuration file.
459          *
460          * @param newConfig
461          *            {@code true} if Sone was started with a fresh configuration,
462          *            {@code false} if the existing configuration could be read
463          */
464         public void setNewConfig(boolean newConfig) {
465                 if (newConfig && !hasFirstStartNotification()) {
466                         Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
467                         Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
468                         notificationManager.addNotification(configNotReadNotification);
469                 }
470         }
471
472         //
473         // PRIVATE ACCESSORS
474         //
475
476         /**
477          * Returns whether the first start notification is currently displayed.
478          *
479          * @return {@code true} if the first-start notification is currently
480          *         displayed, {@code false} otherwise
481          */
482         private boolean hasFirstStartNotification() {
483                 return notificationManager.getNotification("first-start-notification") != null;
484         }
485
486         //
487         // ACTIONS
488         //
489
490         /**
491          * Starts the web interface and registers all toadlets.
492          */
493         public void start() {
494                 registerToadlets();
495
496                 /* notification templates. */
497                 Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
498
499                 final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
500                 notificationManager.addNotification(startupNotification);
501
502                 ticker.schedule(new Runnable() {
503
504                         @Override
505                         public void run() {
506                                 startupNotification.dismiss();
507                         }
508                 }, 2, TimeUnit.MINUTES);
509
510                 Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
511                 final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
512                 ticker.scheduleAtFixedRate(new Runnable() {
513
514                         @Override
515                         @SuppressWarnings("synthetic-access")
516                         public void run() {
517                                 if (getCore().getIdentityManager().isConnected()) {
518                                         wotMissingNotification.dismiss();
519                                 } else {
520                                         notificationManager.addNotification(wotMissingNotification);
521                                 }
522                         }
523
524                 }, 15, 15, TimeUnit.SECONDS);
525         }
526
527         /**
528          * Stops the web interface and unregisters all toadlets.
529          */
530         public void stop() {
531                 pageToadletRegistry.unregisterToadlets();
532                 ticker.shutdownNow();
533         }
534
535         //
536         // PRIVATE METHODS
537         //
538
539         /**
540          * Register all toadlets.
541          */
542         private void registerToadlets() {
543                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
544                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
545                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
546
547                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
548                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
549                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
550                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
551                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
552                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
553                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
554                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
555                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
556                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
557                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
558                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
559                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
560                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
561                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
562                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
563                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
564                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
565                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
566                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
567                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
568                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
569                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
570                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
571                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
572                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
573                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
574                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
575                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
576                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
577                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
578                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
579                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
580                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
581                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
582                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
583                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
584                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
585                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
586                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
587                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
588                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
589                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
590                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
591                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
592                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
593                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
594                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
595                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
596                 pageToadletRegistry.addPage(new GetImagePage(this));
597                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
598                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
599                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
600                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
601                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
602                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
603                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
604                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
605                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
606                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
607                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
608                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
609                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
610                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
611                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
612                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
613                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
614                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
615                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
616                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
617                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
618                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
619                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
620                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
621                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
622                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
623                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
624
625                 pageToadletRegistry.registerToadlets();
626         }
627
628         /**
629          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
630          * {@link SonePart}s in the given text (after parsing it using
631          * {@link SoneTextParser}).
632          *
633          * @param text
634          *            The text to parse
635          * @return All mentioned local Sones
636          */
637         private Collection<Sone> getMentionedSones(String text) {
638                 /* we need no context to find mentioned Sones. */
639                 Set<Sone> mentionedSones = new HashSet<>();
640                 for (Part part : soneTextParser.parse(text, null)) {
641                         if (part instanceof SonePart) {
642                                 mentionedSones.add(((SonePart) part).getSone());
643                         }
644                 }
645                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
646         }
647
648         /**
649          * Returns the Sone insert notification for the given Sone. If no
650          * notification for the given Sone exists, a new notification is created and
651          * cached.
652          *
653          * @param sone
654          *            The Sone to get the insert notification for
655          * @return The Sone insert notification
656          */
657         private TemplateNotification getSoneInsertNotification(Sone sone) {
658                 synchronized (soneInsertNotifications) {
659                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
660                         if (templateNotification == null) {
661                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
662                                 templateNotification.set("insertSone", sone);
663                                 soneInsertNotifications.put(sone, templateNotification);
664                         }
665                         return templateNotification;
666                 }
667         }
668
669         private boolean localSoneMentionedInNewPostOrReply(Post post) {
670                 if (!post.getSone().isLocal()) {
671                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
672                                 return true;
673                         }
674                 }
675                 for (PostReply postReply : getCore().getReplies(post.getId())) {
676                         if (postReply.getSone().isLocal()) {
677                                 continue;
678                         }
679                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
680                                 return true;
681                         }
682                 }
683                 return false;
684         }
685
686         //
687         // EVENT HANDLERS
688         //
689
690         /**
691          * Notifies the web interface that a new {@link Sone} was found.
692          *
693          * @param newSoneFoundEvent
694          *            The event
695          */
696         @Subscribe
697         public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
698                 newSoneNotification.add(newSoneFoundEvent.getSone());
699                 if (!hasFirstStartNotification()) {
700                         notificationManager.addNotification(newSoneNotification);
701                 }
702         }
703
704         /**
705          * Notifies the web interface that a new {@link Post} was found.
706          *
707          * @param newPostFoundEvent
708          *            The event
709          */
710         @Subscribe
711         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
712                 Post post = newPostFoundEvent.getPost();
713                 boolean isLocal = post.getSone().isLocal();
714                 if (isLocal) {
715                         localPostNotification.add(post);
716                 } else {
717                         newPostNotification.add(post);
718                 }
719                 if (!hasFirstStartNotification()) {
720                         notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
721                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
722                                 mentionNotification.add(post);
723                                 notificationManager.addNotification(mentionNotification);
724                         }
725                 } else {
726                         getCore().markPostKnown(post);
727                 }
728         }
729
730         /**
731          * Notifies the web interface that a new {@link PostReply} was found.
732          *
733          * @param newPostReplyFoundEvent
734          *            The event
735          */
736         @Subscribe
737         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
738                 PostReply reply = newPostReplyFoundEvent.getPostReply();
739                 boolean isLocal = reply.getSone().isLocal();
740                 if (isLocal) {
741                         localReplyNotification.add(reply);
742                 } else {
743                         newReplyNotification.add(reply);
744                 }
745                 if (!hasFirstStartNotification()) {
746                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
747                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
748                                 mentionNotification.add(reply.getPost().get());
749                                 notificationManager.addNotification(mentionNotification);
750                         }
751                 } else {
752                         getCore().markReplyKnown(reply);
753                 }
754         }
755
756         /**
757          * Notifies the web interface that a {@link Sone} was marked as known.
758          *
759          * @param markSoneKnownEvent
760          *            The event
761          */
762         @Subscribe
763         public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
764                 newSoneNotification.remove(markSoneKnownEvent.getSone());
765         }
766
767         @Subscribe
768         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
769                 removePost(markPostKnownEvent.getPost());
770         }
771
772         @Subscribe
773         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
774                 removeReply(markPostReplyKnownEvent.getPostReply());
775         }
776
777         @Subscribe
778         public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
779                 newSoneNotification.remove(soneRemovedEvent.getSone());
780         }
781
782         @Subscribe
783         public void postRemoved(PostRemovedEvent postRemovedEvent) {
784                 removePost(postRemovedEvent.getPost());
785         }
786
787         private void removePost(Post post) {
788                 newPostNotification.remove(post);
789                 localPostNotification.remove(post);
790                 if (!localSoneMentionedInNewPostOrReply(post)) {
791                         mentionNotification.remove(post);
792                 }
793         }
794
795         @Subscribe
796         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
797                 removeReply(postReplyRemovedEvent.getPostReply());
798         }
799
800         private void removeReply(PostReply reply) {
801                 newReplyNotification.remove(reply);
802                 localReplyNotification.remove(reply);
803                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
804                         mentionNotification.remove(reply.getPost().get());
805                 }
806         }
807
808         /**
809          * Notifies the web interface that a Sone was locked.
810          *
811          * @param soneLockedEvent
812          *            The event
813          */
814         @Subscribe
815         public void soneLocked(SoneLockedEvent soneLockedEvent) {
816                 final Sone sone = soneLockedEvent.getSone();
817                 ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
818
819                         @Override
820                         @SuppressWarnings("synthetic-access")
821                         public void run() {
822                                 lockedSonesNotification.add(sone);
823                                 notificationManager.addNotification(lockedSonesNotification);
824                         }
825                 }, 5, TimeUnit.MINUTES);
826                 lockedSonesTickerObjects.put(sone, tickerObject);
827         }
828
829         /**
830          * Notifies the web interface that a Sone was unlocked.
831          *
832          * @param soneUnlockedEvent
833          *            The event
834          */
835         @Subscribe
836         public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
837                 lockedSonesNotification.remove(soneUnlockedEvent.getSone());
838                 lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
839         }
840
841         /**
842          * Notifies the web interface that a {@link Sone} is being inserted.
843          *
844          * @param soneInsertingEvent
845          *            The event
846          */
847         @Subscribe
848         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
849                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
850                 soneInsertNotification.set("soneStatus", "inserting");
851                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
852                         notificationManager.addNotification(soneInsertNotification);
853                 }
854         }
855
856         /**
857          * Notifies the web interface that a {@link Sone} was inserted.
858          *
859          * @param soneInsertedEvent
860          *            The event
861          */
862         @Subscribe
863         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
864                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
865                 soneInsertNotification.set("soneStatus", "inserted");
866                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
867                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
868                         notificationManager.addNotification(soneInsertNotification);
869                 }
870         }
871
872         /**
873          * Notifies the web interface that a {@link Sone} insert was aborted.
874          *
875          * @param soneInsertAbortedEvent
876          *            The event
877          */
878         @Subscribe
879         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
880                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
881                 soneInsertNotification.set("soneStatus", "insert-aborted");
882                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
883                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
884                         notificationManager.addNotification(soneInsertNotification);
885                 }
886         }
887
888         /**
889          * Notifies the web interface that a new Sone version was found.
890          *
891          * @param updateFoundEvent
892          *            The event
893          */
894         @Subscribe
895         public void updateFound(UpdateFoundEvent updateFoundEvent) {
896                 newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
897                 newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
898                 newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
899                 newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
900                 notificationManager.addNotification(newVersionNotification);
901         }
902
903         /**
904          * Notifies the web interface that an image insert was started
905          *
906          * @param imageInsertStartedEvent
907          *            The event
908          */
909         @Subscribe
910         public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
911                 insertingImagesNotification.add(imageInsertStartedEvent.getImage());
912                 notificationManager.addNotification(insertingImagesNotification);
913         }
914
915         /**
916          * Notifies the web interface that an {@link Image} insert was aborted.
917          *
918          * @param imageInsertAbortedEvent
919          *            The event
920          */
921         @Subscribe
922         public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
923                 insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
924         }
925
926         /**
927          * Notifies the web interface that an {@link Image} insert is finished.
928          *
929          * @param imageInsertFinishedEvent
930          *            The event
931          */
932         @Subscribe
933         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
934                 insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
935                 insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
936                 notificationManager.addNotification(insertedImagesNotification);
937         }
938
939         /**
940          * Notifies the web interface that an {@link Image} insert has failed.
941          *
942          * @param imageInsertFailedEvent
943          *            The event
944          */
945         @Subscribe
946         public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
947                 insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
948                 imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
949                 notificationManager.addNotification(imageInsertFailedNotification);
950         }
951
952         @Subscribe
953         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
954                 pageToadletRegistry.activateDebugMode();
955         }
956
957 }