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