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