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