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