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