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