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