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