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