✨ Only show metrics page if debug flag is set
[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                 if (getCore().getDebugInformation().getShowMetrics()) {
620                         pageToadletRegistry.addPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
621                 }
622                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
623                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
624                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
625                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
626                 pageToadletRegistry.addPage(new GetImagePage(this));
627                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
628                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
629                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
630                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
631                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
632                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
633                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
634                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
635                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
636                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
637                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
638                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
639                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
640                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
641                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
642                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
643                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
644                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
645                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
646                 pageToadletRegistry.addPage(new TrustAjaxPage(this));
647                 pageToadletRegistry.addPage(new DistrustAjaxPage(this));
648                 pageToadletRegistry.addPage(new UntrustAjaxPage(this));
649                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
650                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
651                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
652                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
653                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
654                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
655                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
656                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
657
658                 pageToadletRegistry.registerToadlets();
659         }
660
661         /**
662          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
663          * {@link SonePart}s in the given text (after parsing it using
664          * {@link SoneTextParser}).
665          *
666          * @param text
667          *            The text to parse
668          * @return All mentioned local Sones
669          */
670         private Collection<Sone> getMentionedSones(String text) {
671                 /* we need no context to find mentioned Sones. */
672                 Set<Sone> mentionedSones = new HashSet<>();
673                 for (Part part : soneTextParser.parse(text, null)) {
674                         if (part instanceof SonePart) {
675                                 mentionedSones.add(((SonePart) part).getSone());
676                         }
677                 }
678                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
679         }
680
681         /**
682          * Returns the Sone insert notification for the given Sone. If no
683          * notification for the given Sone exists, a new notification is created and
684          * cached.
685          *
686          * @param sone
687          *            The Sone to get the insert notification for
688          * @return The Sone insert notification
689          */
690         private TemplateNotification getSoneInsertNotification(Sone sone) {
691                 synchronized (soneInsertNotifications) {
692                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
693                         if (templateNotification == null) {
694                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
695                                 templateNotification.set("insertSone", sone);
696                                 soneInsertNotifications.put(sone, templateNotification);
697                         }
698                         return templateNotification;
699                 }
700         }
701
702         private boolean localSoneMentionedInNewPostOrReply(Post post) {
703                 if (!post.getSone().isLocal()) {
704                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
705                                 return true;
706                         }
707                 }
708                 for (PostReply postReply : getCore().getReplies(post.getId())) {
709                         if (postReply.getSone().isLocal()) {
710                                 continue;
711                         }
712                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
713                                 return true;
714                         }
715                 }
716                 return false;
717         }
718
719         //
720         // EVENT HANDLERS
721         //
722
723         /**
724          * Notifies the web interface that a new {@link Sone} was found.
725          *
726          * @param newSoneFoundEvent
727          *            The event
728          */
729         @Subscribe
730         public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
731                 newSoneNotification.add(newSoneFoundEvent.getSone());
732                 if (!hasFirstStartNotification()) {
733                         notificationManager.addNotification(newSoneNotification);
734                 }
735         }
736
737         /**
738          * Notifies the web interface that a new {@link Post} was found.
739          *
740          * @param newPostFoundEvent
741          *            The event
742          */
743         @Subscribe
744         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
745                 Post post = newPostFoundEvent.getPost();
746                 boolean isLocal = post.getSone().isLocal();
747                 if (isLocal) {
748                         localPostNotification.add(post);
749                 } else {
750                         newPostNotification.add(post);
751                 }
752                 if (!hasFirstStartNotification()) {
753                         notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
754                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
755                                 mentionNotification.add(post);
756                                 notificationManager.addNotification(mentionNotification);
757                         }
758                 } else {
759                         getCore().markPostKnown(post);
760                 }
761         }
762
763         /**
764          * Notifies the web interface that a new {@link PostReply} was found.
765          *
766          * @param newPostReplyFoundEvent
767          *            The event
768          */
769         @Subscribe
770         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
771                 PostReply reply = newPostReplyFoundEvent.getPostReply();
772                 boolean isLocal = reply.getSone().isLocal();
773                 if (isLocal) {
774                         localReplyNotification.add(reply);
775                 } else {
776                         newReplyNotification.add(reply);
777                 }
778                 if (!hasFirstStartNotification()) {
779                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
780                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
781                                 mentionNotification.add(reply.getPost().get());
782                                 notificationManager.addNotification(mentionNotification);
783                         }
784                 } else {
785                         getCore().markReplyKnown(reply);
786                 }
787         }
788
789         /**
790          * Notifies the web interface that a {@link Sone} was marked as known.
791          *
792          * @param markSoneKnownEvent
793          *            The event
794          */
795         @Subscribe
796         public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
797                 newSoneNotification.remove(markSoneKnownEvent.getSone());
798         }
799
800         @Subscribe
801         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
802                 removePost(markPostKnownEvent.getPost());
803         }
804
805         @Subscribe
806         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
807                 removeReply(markPostReplyKnownEvent.getPostReply());
808         }
809
810         @Subscribe
811         public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
812                 newSoneNotification.remove(soneRemovedEvent.getSone());
813         }
814
815         @Subscribe
816         public void postRemoved(PostRemovedEvent postRemovedEvent) {
817                 removePost(postRemovedEvent.getPost());
818         }
819
820         private void removePost(Post post) {
821                 newPostNotification.remove(post);
822                 localPostNotification.remove(post);
823                 if (!localSoneMentionedInNewPostOrReply(post)) {
824                         mentionNotification.remove(post);
825                 }
826         }
827
828         @Subscribe
829         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
830                 removeReply(postReplyRemovedEvent.getPostReply());
831         }
832
833         private void removeReply(PostReply reply) {
834                 newReplyNotification.remove(reply);
835                 localReplyNotification.remove(reply);
836                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
837                         mentionNotification.remove(reply.getPost().get());
838                 }
839         }
840
841         /**
842          * Notifies the web interface that a Sone was locked.
843          *
844          * @param soneLockedEvent
845          *            The event
846          */
847         @Subscribe
848         public void soneLocked(SoneLockedEvent soneLockedEvent) {
849                 final Sone sone = soneLockedEvent.getSone();
850                 ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
851
852                         @Override
853                         @SuppressWarnings("synthetic-access")
854                         public void run() {
855                                 lockedSonesNotification.add(sone);
856                                 notificationManager.addNotification(lockedSonesNotification);
857                         }
858                 }, 5, TimeUnit.MINUTES);
859                 lockedSonesTickerObjects.put(sone, tickerObject);
860         }
861
862         /**
863          * Notifies the web interface that a Sone was unlocked.
864          *
865          * @param soneUnlockedEvent
866          *            The event
867          */
868         @Subscribe
869         public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
870                 lockedSonesNotification.remove(soneUnlockedEvent.getSone());
871                 lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
872         }
873
874         /**
875          * Notifies the web interface that a {@link Sone} is being inserted.
876          *
877          * @param soneInsertingEvent
878          *            The event
879          */
880         @Subscribe
881         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
882                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
883                 soneInsertNotification.set("soneStatus", "inserting");
884                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
885                         notificationManager.addNotification(soneInsertNotification);
886                 }
887         }
888
889         /**
890          * Notifies the web interface that a {@link Sone} was inserted.
891          *
892          * @param soneInsertedEvent
893          *            The event
894          */
895         @Subscribe
896         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
897                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
898                 soneInsertNotification.set("soneStatus", "inserted");
899                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
900                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
901                         notificationManager.addNotification(soneInsertNotification);
902                 }
903         }
904
905         /**
906          * Notifies the web interface that a {@link Sone} insert was aborted.
907          *
908          * @param soneInsertAbortedEvent
909          *            The event
910          */
911         @Subscribe
912         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
913                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
914                 soneInsertNotification.set("soneStatus", "insert-aborted");
915                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
916                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
917                         notificationManager.addNotification(soneInsertNotification);
918                 }
919         }
920
921         /**
922          * Notifies the web interface that a new Sone version was found.
923          *
924          * @param updateFoundEvent
925          *            The event
926          */
927         @Subscribe
928         public void updateFound(UpdateFoundEvent updateFoundEvent) {
929                 newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
930                 newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
931                 newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
932                 newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
933                 notificationManager.addNotification(newVersionNotification);
934         }
935
936         /**
937          * Notifies the web interface that an image insert was started
938          *
939          * @param imageInsertStartedEvent
940          *            The event
941          */
942         @Subscribe
943         public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
944                 insertingImagesNotification.add(imageInsertStartedEvent.getImage());
945                 notificationManager.addNotification(insertingImagesNotification);
946         }
947
948         /**
949          * Notifies the web interface that an {@link Image} insert was aborted.
950          *
951          * @param imageInsertAbortedEvent
952          *            The event
953          */
954         @Subscribe
955         public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
956                 insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
957         }
958
959         /**
960          * Notifies the web interface that an {@link Image} insert is finished.
961          *
962          * @param imageInsertFinishedEvent
963          *            The event
964          */
965         @Subscribe
966         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
967                 insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
968                 insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
969                 notificationManager.addNotification(insertedImagesNotification);
970         }
971
972         /**
973          * Notifies the web interface that an {@link Image} insert has failed.
974          *
975          * @param imageInsertFailedEvent
976          *            The event
977          */
978         @Subscribe
979         public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
980                 insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
981                 imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
982                 notificationManager.addNotification(imageInsertFailedNotification);
983         }
984
985 }