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