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