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