X-Git-Url: https://git.pterodactylus.net/?a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fweb%2FWebInterface.java;h=1c429e7afc6b932c25e6838736dcd712f5d5fc55;hb=c9e306ac8e3ada846e87a0cc256a20fc148f381c;hp=774fe7ac5d88944a775963b1acc30c71631f78be;hpb=93ea75542313445d34da309e81384a123c361fab;p=Sone.git diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 774fe7a..f8b9c5a 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -1,5 +1,5 @@ /* - * FreenetSone - WebInterface.java - Copyright © 2010 David Roden + * Sone - WebInterface.java - Copyright © 2010 David Roden * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,30 +17,132 @@ package net.pterodactylus.sone.web; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.io.StringReader; import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import net.pterodactylus.sone.core.Core; +import net.pterodactylus.sone.core.CoreListener; +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Reply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.L10nFilter; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.sone.web.page.CSSPage; +import net.pterodactylus.sone.notify.ListNotification; +import net.pterodactylus.sone.template.AlbumAccessor; +import net.pterodactylus.sone.template.CollectionAccessor; +import net.pterodactylus.sone.template.CssClassNameFilter; +import net.pterodactylus.sone.template.HttpRequestAccessor; +import net.pterodactylus.sone.template.IdentityAccessor; +import net.pterodactylus.sone.template.ImageAccessor; +import net.pterodactylus.sone.template.ImageLinkFilter; +import net.pterodactylus.sone.template.JavascriptFilter; +import net.pterodactylus.sone.template.ParserFilter; +import net.pterodactylus.sone.template.PostAccessor; +import net.pterodactylus.sone.template.ProfileAccessor; +import net.pterodactylus.sone.template.ReplyAccessor; +import net.pterodactylus.sone.template.ReplyGroupFilter; +import net.pterodactylus.sone.template.RequestChangeFilter; +import net.pterodactylus.sone.template.SoneAccessor; +import net.pterodactylus.sone.template.SubstringFilter; +import net.pterodactylus.sone.template.TrustAccessor; +import net.pterodactylus.sone.template.UniqueElementFilter; +import net.pterodactylus.sone.template.UnknownDateFilter; +import net.pterodactylus.sone.text.Part; +import net.pterodactylus.sone.text.SonePart; +import net.pterodactylus.sone.text.SoneTextParser; +import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage; +import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage; +import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage; +import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage; +import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage; +import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage; +import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage; +import net.pterodactylus.sone.web.ajax.DistrustAjaxPage; +import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage; +import net.pterodactylus.sone.web.ajax.EditImageAjaxPage; +import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage; +import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage; +import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage; +import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage; +import net.pterodactylus.sone.web.ajax.GetPostAjaxPage; +import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage; +import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage; +import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage; +import net.pterodactylus.sone.web.ajax.GetTranslationPage; +import net.pterodactylus.sone.web.ajax.LikeAjaxPage; +import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage; +import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage; +import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage; +import net.pterodactylus.sone.web.ajax.TrustAjaxPage; +import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage; +import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage; +import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage; +import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage; +import net.pterodactylus.sone.web.ajax.UntrustAjaxPage; +import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.sone.web.page.PageToadlet; import net.pterodactylus.sone.web.page.PageToadletFactory; +import net.pterodactylus.util.cache.Cache; +import net.pterodactylus.util.cache.CacheException; +import net.pterodactylus.util.cache.CacheItem; +import net.pterodactylus.util.cache.DefaultCacheItem; +import net.pterodactylus.util.cache.MemoryCache; +import net.pterodactylus.util.cache.ValueRetriever; +import net.pterodactylus.util.collection.SetBuilder; +import net.pterodactylus.util.filter.Filters; import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.service.AbstractService; +import net.pterodactylus.util.notify.Notification; +import net.pterodactylus.util.notify.NotificationManager; +import net.pterodactylus.util.notify.TemplateNotification; +import net.pterodactylus.util.template.CollectionSortFilter; +import net.pterodactylus.util.template.ContainsFilter; +import net.pterodactylus.util.template.DateFilter; +import net.pterodactylus.util.template.FormatFilter; +import net.pterodactylus.util.template.HtmlFilter; +import net.pterodactylus.util.template.MatchFilter; +import net.pterodactylus.util.template.ModFilter; +import net.pterodactylus.util.template.Provider; +import net.pterodactylus.util.template.ReflectionAccessor; +import net.pterodactylus.util.template.ReplaceFilter; +import net.pterodactylus.util.template.StoreFilter; import net.pterodactylus.util.template.Template; -import net.pterodactylus.util.template.TemplateFactory; -import freenet.clients.http.LinkEnabledCallback; +import net.pterodactylus.util.template.TemplateContext; +import net.pterodactylus.util.template.TemplateContextFactory; +import net.pterodactylus.util.template.TemplateException; +import net.pterodactylus.util.template.TemplateParser; +import net.pterodactylus.util.template.XmlFilter; +import net.pterodactylus.util.thread.Ticker; +import net.pterodactylus.util.version.Version; +import net.pterodactylus.util.web.RedirectPage; +import net.pterodactylus.util.web.StaticPage; +import net.pterodactylus.util.web.TemplatePage; import freenet.clients.http.SessionManager; +import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContainer; import freenet.clients.http.ToadletContext; import freenet.l10n.BaseL10n; +import freenet.support.api.HTTPRequest; /** * Bundles functionality that a web interface of a Freenet plugin needs, e.g. @@ -48,25 +150,153 @@ import freenet.l10n.BaseL10n; * * @author David ‘Bombe’ Roden */ -public class WebInterface extends AbstractService { +public class WebInterface implements CoreListener { /** The logger. */ private static final Logger logger = Logging.getLogger(WebInterface.class); + /** The notification manager. */ + private final NotificationManager notificationManager = new NotificationManager(); + /** The Sone plugin. */ private final SonePlugin sonePlugin; /** The registered toadlets. */ private final List pageToadlets = new ArrayList(); + /** The form password. */ + private final String formPassword; + + /** The template context factory. */ + private final TemplateContextFactory templateContextFactory; + + /** The Sone text parser. */ + private final SoneTextParser soneTextParser; + + /** The parser filter. */ + private final ParserFilter parserFilter; + + /** The “new Sone” notification. */ + private final ListNotification newSoneNotification; + + /** The “new post” notification. */ + private final ListNotification newPostNotification; + + /** The “new reply” notification. */ + private final ListNotification newReplyNotification; + + /** The invisible “local post” notification. */ + private final ListNotification localPostNotification; + + /** The invisible “local reply” notification. */ + private final ListNotification localReplyNotification; + + /** The “you have been mentioned” notification. */ + private final ListNotification mentionNotification; + + /** Notifications for sone inserts. */ + private final Map soneInsertNotifications = new HashMap(); + + /** Sone locked notification ticker objects. */ + private final Map lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap()); + + /** The “Sone locked” notification. */ + private final ListNotification lockedSonesNotification; + + /** The “new version” notification. */ + private final TemplateNotification newVersionNotification; + + /** The “inserting images” notification. */ + private final ListNotification insertingImagesNotification; + + /** The “inserted images” notification. */ + private final ListNotification insertedImagesNotification; + + /** The “image insert failed” notification. */ + private final ListNotification imageInsertFailedNotification; + /** * Creates a new web interface. * * @param sonePlugin * The Sone plugin */ + @SuppressWarnings("synthetic-access") public WebInterface(SonePlugin sonePlugin) { this.sonePlugin = sonePlugin; + formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword(); + soneTextParser = new SoneTextParser(getCore(), getCore()); + + templateContextFactory = new TemplateContextFactory(); + templateContextFactory.addAccessor(Object.class, new ReflectionAccessor()); + templateContextFactory.addAccessor(Collection.class, new CollectionAccessor()); + templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore())); + templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore())); + templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore())); + templateContextFactory.addAccessor(Album.class, new AlbumAccessor()); + templateContextFactory.addAccessor(Image.class, new ImageAccessor()); + templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore())); + templateContextFactory.addAccessor(Trust.class, new TrustAccessor()); + templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor()); + templateContextFactory.addAccessor(Profile.class, new ProfileAccessor(getCore())); + templateContextFactory.addFilter("date", new DateFilter()); + templateContextFactory.addFilter("html", new HtmlFilter()); + templateContextFactory.addFilter("replace", new ReplaceFilter()); + templateContextFactory.addFilter("store", new StoreFilter()); + templateContextFactory.addFilter("l10n", new L10nFilter(this)); + templateContextFactory.addFilter("substring", new SubstringFilter()); + templateContextFactory.addFilter("xml", new XmlFilter()); + templateContextFactory.addFilter("change", new RequestChangeFilter()); + templateContextFactory.addFilter("match", new MatchFilter()); + templateContextFactory.addFilter("css", new CssClassNameFilter()); + templateContextFactory.addFilter("js", new JavascriptFilter()); + templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), templateContextFactory, soneTextParser)); + templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate")); + templateContextFactory.addFilter("format", new FormatFilter()); + templateContextFactory.addFilter("sort", new CollectionSortFilter()); + templateContextFactory.addFilter("image-link", new ImageLinkFilter(getCore(), templateContextFactory)); + templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter()); + templateContextFactory.addFilter("in", new ContainsFilter()); + templateContextFactory.addFilter("unique", new UniqueElementFilter()); + templateContextFactory.addFilter("mod", new ModFilter()); + templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER); + templateContextFactory.addProvider(new ClassPathTemplateProvider()); + templateContextFactory.addTemplateObject("webInterface", this); + templateContextFactory.addTemplateObject("formPassword", formPassword); + + /* create notifications. */ + Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html")); + newSoneNotification = new ListNotification("new-sone-notification", "sones", newSoneNotificationTemplate, false); + + Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html")); + newPostNotification = new ListNotification("new-post-notification", "posts", newPostNotificationTemplate, false); + + Template localPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html")); + localPostNotification = new ListNotification("local-post-notification", "posts", localPostNotificationTemplate, false); + + Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html")); + newReplyNotification = new ListNotification("new-reply-notification", "replies", newReplyNotificationTemplate, false); + + Template localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html")); + localReplyNotification = new ListNotification("local-reply-notification", "replies", localReplyNotificationTemplate, false); + + Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html")); + mentionNotification = new ListNotification("mention-notification", "posts", mentionNotificationTemplate, false); + + Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html")); + lockedSonesNotification = new ListNotification("sones-locked-notification", "sones", lockedSonesTemplate); + + Template newVersionTemplate = TemplateParser.parse(createReader("/templates/notify/newVersionNotification.html")); + newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate); + + Template insertingImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserting-images-notification.html")); + insertingImagesNotification = new ListNotification("inserting-images-notification", "images", insertingImagesTemplate); + + Template insertedImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserted-images-notification.html")); + insertedImagesNotification = new ListNotification("inserted-images-notification", "images", insertedImagesTemplate); + + Template imageInsertFailedTemplate = TemplateParser.parse(createReader("/templates/notify/image-insert-failed-notification.html")); + imageInsertFailedNotification = new ListNotification("image-insert-failed-notification", "images", imageInsertFailedTemplate); } // @@ -74,11 +304,138 @@ public class WebInterface extends AbstractService { // /** + * Returns the Sone core used by the Sone plugin. + * + * @return The Sone core + */ + public Core getCore() { + return sonePlugin.core(); + } + + /** + * Returns the template context factory of the web interface. + * + * @return The template context factory + */ + public TemplateContextFactory getTemplateContextFactory() { + return templateContextFactory; + } + + /** + * Returns the current session, creating a new session if there is no + * current session. + * + * @param toadletContenxt + * The toadlet context + * @return The current session, or {@code null} if there is no current + * session + */ + public Session getCurrentSession(ToadletContext toadletContenxt) { + return getCurrentSession(toadletContenxt, true); + } + + /** + * Returns the current session, creating a new session if there is no + * current session and {@code create} is {@code true}. + * + * @param toadletContenxt + * The toadlet context + * @param create + * {@code true} to create a new session if there is no current + * session, {@code false} otherwise + * @return The current session, or {@code null} if there is no current + * session + */ + public Session getCurrentSession(ToadletContext toadletContenxt, boolean create) { + Session session = getSessionManager().useSession(toadletContenxt); + if (create && (session == null)) { + session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt); + } + return session; + } + + /** + * Returns the currently logged in Sone. + * + * @param toadletContext + * The toadlet context + * @return The currently logged in Sone, or {@code null} if no Sone is + * currently logged in + */ + public Sone getCurrentSone(ToadletContext toadletContext) { + return getCurrentSone(toadletContext, true); + } + + /** + * Returns the currently logged in Sone. + * + * @param toadletContext + * The toadlet context + * @param create + * {@code true} to create a new session if no session exists, + * {@code false} to not create a new session + * @return The currently logged in Sone, or {@code null} if no Sone is + * currently logged in + */ + public Sone getCurrentSone(ToadletContext toadletContext, boolean create) { + Set localSones = getCore().getLocalSones(); + if (localSones.size() == 1) { + return localSones.iterator().next(); + } + return getCurrentSone(getCurrentSession(toadletContext, create)); + } + + /** + * Returns the currently logged in Sone. + * + * @param session + * The session + * @return The currently logged in Sone, or {@code null} if no Sone is + * currently logged in + */ + public Sone getCurrentSone(Session session) { + if (session == null) { + return null; + } + String soneId = (String) session.getAttribute("Sone.CurrentSone"); + if (soneId == null) { + return null; + } + return getCore().getLocalSone(soneId, false); + } + + /** + * Sets the currently logged in Sone. + * + * @param toadletContext + * The toadlet context + * @param sone + * The Sone to set as currently logged in + */ + public void setCurrentSone(ToadletContext toadletContext, Sone sone) { + Session session = getCurrentSession(toadletContext); + if (sone == null) { + session.removeAttribute("Sone.CurrentSone"); + } else { + session.setAttribute("Sone.CurrentSone", sone.getId()); + } + } + + /** + * Returns the notification manager. + * + * @return The notification manager + */ + public NotificationManager getNotifications() { + return notificationManager; + } + + /** * Returns the l10n helper of the node. * * @return The node’s l10n helper */ - public BaseL10n l10n() { + public BaseL10n getL10n() { return sonePlugin.l10n().getBase(); } @@ -87,33 +444,132 @@ public class WebInterface extends AbstractService { * * @return The node’s session manager */ - public SessionManager sessionManager() { - try { - return sonePlugin.pluginRespirator().getSessionManager(new URI("/")); - } catch (URISyntaxException use1) { - logger.log(Level.SEVERE, "Could not get Session Manager!", use1); - return null; + public SessionManager getSessionManager() { + return sonePlugin.pluginRespirator().getSessionManager("Sone"); + } + + /** + * Returns the node’s form password. + * + * @return The form password + */ + public String getFormPassword() { + return formPassword; + } + + /** + * Returns the posts that have been announced as new in the + * {@link #newPostNotification}. + * + * @return The new posts + */ + public Set getNewPosts() { + return new SetBuilder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).get(); + } + + /** + * Returns the replies that have been announced as new in the + * {@link #newReplyNotification}. + * + * @return The new replies + */ + public Set getNewReplies() { + return new SetBuilder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).get(); + } + + /** + * Sets whether the current start of the plugin is the first start. It is + * considered a first start if the configuration file does not exist. + * + * @param firstStart + * {@code true} if no configuration file existed when Sone was + * loaded, {@code false} otherwise + */ + public void setFirstStart(boolean firstStart) { + if (firstStart) { + Template firstStartNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/firstStartNotification.html")); + Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate); + notificationManager.addNotification(firstStartNotification); + } + } + + /** + * Sets whether Sone was started with a fresh configuration file. + * + * @param newConfig + * {@code true} if Sone was started with a fresh configuration, + * {@code false} if the existing configuration could be read + */ + public void setNewConfig(boolean newConfig) { + if (newConfig && !hasFirstStartNotification()) { + Template configNotReadNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/configNotReadNotification.html")); + Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate); + notificationManager.addNotification(configNotReadNotification); } } // - // SERVICE METHODS + // PRIVATE ACCESSORS // /** - * {@inheritDoc} + * Returns whether the first start notification is currently displayed. + * + * @return {@code true} if the first-start notification is currently + * displayed, {@code false} otherwise */ - @Override - protected void serviceStart() { + private boolean hasFirstStartNotification() { + return notificationManager.getNotification("first-start-notification") != null; + } + + // + // ACTIONS + // + + /** + * Starts the web interface and registers all toadlets. + */ + public void start() { registerToadlets(); + + /* notification templates. */ + Template startupNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/startupNotification.html")); + + final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate); + notificationManager.addNotification(startupNotification); + + Ticker.getInstance().registerEvent(System.currentTimeMillis() + (120 * 1000), new Runnable() { + + @Override + public void run() { + startupNotification.dismiss(); + } + }, "Sone Startup Notification Remover"); + + Template wotMissingNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/wotMissingNotification.html")); + final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate); + Ticker.getInstance().registerEvent(System.currentTimeMillis() + (15 * 1000), new Runnable() { + + @Override + @SuppressWarnings("synthetic-access") + public void run() { + if (getCore().getIdentityManager().isConnected()) { + wotMissingNotification.dismiss(); + } else { + notificationManager.addNotification(wotMissingNotification); + } + Ticker.getInstance().registerEvent(System.currentTimeMillis() + (15 * 1000), this, "Sone WoT Connector Checker"); + } + + }, "Sone WoT Connector Checker"); } /** - * {@inheritDoc} + * Stops the web interface and unregisters all toadlets. */ - @Override - protected void serviceStop() { + public void stop() { unregisterToadlets(); + Ticker.getInstance().stop(); } // @@ -124,21 +580,123 @@ public class WebInterface extends AbstractService { * Register all toadlets. */ private void registerToadlets() { - TemplateFactory templateFactory = new SoneTemplateFactory(l10n()); - - Template loginTemplate = templateFactory.createTemplate(createReader("/templates/login.html")); - loginTemplate.set("formPassword", sonePlugin.pluginRespirator().getToadletContainer().getFormPassword()); + Template emptyTemplate = TemplateParser.parse(new StringReader("")); + Template loginTemplate = TemplateParser.parse(createReader("/templates/login.html")); + Template indexTemplate = TemplateParser.parse(createReader("/templates/index.html")); + Template newTemplate = TemplateParser.parse(createReader("/templates/new.html")); + Template knownSonesTemplate = TemplateParser.parse(createReader("/templates/knownSones.html")); + Template createSoneTemplate = TemplateParser.parse(createReader("/templates/createSone.html")); + Template createPostTemplate = TemplateParser.parse(createReader("/templates/createPost.html")); + Template createReplyTemplate = TemplateParser.parse(createReader("/templates/createReply.html")); + Template bookmarksTemplate = TemplateParser.parse(createReader("/templates/bookmarks.html")); + Template searchTemplate = TemplateParser.parse(createReader("/templates/search.html")); + Template editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html")); + Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html")); + Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html")); + Template viewSoneTemplate = TemplateParser.parse(createReader("/templates/viewSone.html")); + Template viewPostTemplate = TemplateParser.parse(createReader("/templates/viewPost.html")); + Template deletePostTemplate = TemplateParser.parse(createReader("/templates/deletePost.html")); + Template deleteReplyTemplate = TemplateParser.parse(createReader("/templates/deleteReply.html")); + Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html")); + Template imageBrowserTemplate = TemplateParser.parse(createReader("/templates/imageBrowser.html")); + Template createAlbumTemplate = TemplateParser.parse(createReader("/templates/createAlbum.html")); + Template deleteAlbumTemplate = TemplateParser.parse(createReader("/templates/deleteAlbum.html")); + Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html")); + Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html")); + Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html")); + Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html")); + Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html")); + Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html")); + Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html")); + Template replyTemplate = TemplateParser.parse(createReader("/templates/include/viewReply.html")); + Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml")); PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/"); + pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html"))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new NewPage(newTemplate, this), "New")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfilePage(editProfileTemplate, this), "EditProfile")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldPage(editProfileFieldTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostPage(createPostTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyPage(createReplyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewSonePage(viewSoneTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewPostPage(viewPostTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new LikePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostPage(deletePostTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyPage(deleteReplyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new LockSonePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSonePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new ImageBrowserPage(imageBrowserTemplate, this), "ImageBrowser")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateAlbumPage(createAlbumTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteAlbumPage(deleteAlbumTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UploadImagePage(invalidTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImagePage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteImagePage(deleteImageTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarksPage(bookmarksTemplate, this), "Bookmarks")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SearchPage(searchTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteSonePage(deleteSoneTemplate, this), "DeleteSone")); pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login")); - pageToadlets.add(pageToadletFactory.createPageToadlet(new CSSPage("css/", "/static/css/"))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css"))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript"))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png"))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationsAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetReplyAjaxPage(this, replyTemplate))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetPostAjaxPage(this, postTemplate))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTimesAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new LockSoneAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSoneAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this, parserFilter))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this))); + pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this))); ToadletContainer toadletContainer = sonePlugin.pluginRespirator().getToadletContainer(); - toadletContainer.getPageMaker().addNavigationCategory("/Sone/", "Navigation.Menu.Name", "Navigation.Menu.Tooltip", sonePlugin); + toadletContainer.getPageMaker().addNavigationCategory("/Sone/index.html", "Navigation.Menu.Sone.Name", "Navigation.Menu.Sone.Tooltip", sonePlugin); for (PageToadlet toadlet : pageToadlets) { String menuName = toadlet.getMenuName(); if (menuName != null) { - toadletContainer.register(toadlet, "Navigation.Menu.Name", toadlet.path(), true, "Navigation.Menu.Item." + menuName + ".Name", "Navigation.Menu.Item." + menuName + ".Tooltip", false, new AlwaysEnabledCallback()); + toadletContainer.register(toadlet, "Navigation.Menu.Sone.Name", toadlet.path(), true, "Navigation.Menu.Sone.Item." + menuName + ".Name", "Navigation.Menu.Sone.Item." + menuName + ".Tooltip", false, toadlet); } else { toadletContainer.register(toadlet, null, toadlet.path(), true, false); } @@ -153,7 +711,7 @@ public class WebInterface extends AbstractService { for (PageToadlet pageToadlet : pageToadlets) { toadletContainer.unregister(pageToadlet); } - toadletContainer.getPageMaker().removeNavigationCategory("Navigation.Menu.Name"); + toadletContainer.getPageMaker().removeNavigationCategory("Navigation.Menu.Sone.Name"); } /** @@ -173,21 +731,346 @@ public class WebInterface extends AbstractService { } /** - * {@link LinkEnabledCallback} implementation that always returns - * {@code true} when {@link LinkEnabledCallback#isEnabled(ToadletContext)} - * is called. + * Returns all {@link Core#isLocalSone(Sone) local Sone}s that are + * referenced by {@link SonePart}s in the given text (after parsing it using + * {@link SoneTextParser}). + * + * @param text + * The text to parse + * @return All mentioned local Sones + */ + private Set getMentionedSones(String text) { + /* we need no context to find mentioned Sones. */ + Set mentionedSones = new HashSet(); + try { + for (Part part : soneTextParser.parse(null, new StringReader(text))) { + if (part instanceof SonePart) { + mentionedSones.add(((SonePart) part).getSone()); + } + } + } catch (IOException ioe1) { + logger.log(Level.WARNING, String.format("Could not parse post text: %s", text), ioe1); + } + return Filters.filteredSet(mentionedSones, Sone.LOCAL_SONE_FILTER); + } + + /** + * Returns the Sone insert notification for the given Sone. If no + * notification for the given Sone exists, a new notification is created and + * cached. + * + * @param sone + * The Sone to get the insert notification for + * @return The Sone insert notification + */ + private TemplateNotification getSoneInsertNotification(Sone sone) { + synchronized (soneInsertNotifications) { + TemplateNotification templateNotification = soneInsertNotifications.get(sone); + if (templateNotification == null) { + templateNotification = new TemplateNotification(TemplateParser.parse(createReader("/templates/notify/soneInsertNotification.html"))); + templateNotification.set("insertSone", sone); + soneInsertNotifications.put(sone, templateNotification); + } + return templateNotification; + } + } + + // + // CORELISTENER METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public void newSoneFound(Sone sone) { + newSoneNotification.add(sone); + if (!hasFirstStartNotification()) { + notificationManager.addNotification(newSoneNotification); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void newPostFound(Post post) { + boolean isLocal = getCore().isLocalSone(post.getSone()); + if (isLocal) { + localPostNotification.add(post); + } else { + newPostNotification.add(post); + } + if (!hasFirstStartNotification()) { + notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification); + if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) { + mentionNotification.add(post); + notificationManager.addNotification(mentionNotification); + } + } else { + getCore().markPostKnown(post); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void newReplyFound(PostReply reply) { + boolean isLocal = getCore().isLocalSone(reply.getSone()); + if (isLocal) { + localReplyNotification.add(reply); + } else { + newReplyNotification.add(reply); + } + if (!hasFirstStartNotification()) { + notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification); + if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null) && (reply.getTime() <= System.currentTimeMillis())) { + mentionNotification.add(reply.getPost()); + notificationManager.addNotification(mentionNotification); + } + } else { + getCore().markReplyKnown(reply); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void markSoneKnown(Sone sone) { + newSoneNotification.remove(sone); + } + + /** + * {@inheritDoc} + */ + @Override + public void markPostKnown(Post post) { + newPostNotification.remove(post); + localPostNotification.remove(post); + mentionNotification.remove(post); + } + + /** + * {@inheritDoc} + */ + @Override + public void markReplyKnown(PostReply reply) { + newReplyNotification.remove(reply); + localReplyNotification.remove(reply); + mentionNotification.remove(reply.getPost()); + } + + /** + * {@inheritDoc} + */ + @Override + public void soneRemoved(Sone sone) { + newSoneNotification.remove(sone); + } + + /** + * {@inheritDoc} + */ + @Override + public void postRemoved(Post post) { + newPostNotification.remove(post); + localPostNotification.remove(post); + mentionNotification.remove(post); + } + + /** + * {@inheritDoc} + */ + @Override + public void replyRemoved(PostReply reply) { + newReplyNotification.remove(reply); + localReplyNotification.remove(reply); + if (!getMentionedSones(reply.getText()).isEmpty()) { + boolean isMentioned = false; + for (PostReply existingReply : getCore().getReplies(reply.getPost())) { + isMentioned |= !reply.isKnown() && !getMentionedSones(existingReply.getText()).isEmpty(); + } + if (!isMentioned) { + mentionNotification.remove(reply.getPost()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void soneLocked(final Sone sone) { + Object tickerObject = Ticker.getInstance().registerEvent(System.currentTimeMillis() + (5 * 60) * 1000, new Runnable() { + + @Override + @SuppressWarnings("synthetic-access") + public void run() { + lockedSonesNotification.add(sone); + lockedSonesTickerObjects.remove(sone); + notificationManager.addNotification(lockedSonesNotification); + } + }, "Sone Locked Notification"); + lockedSonesTickerObjects.put(sone, tickerObject); + } + + /** + * {@inheritDoc} + */ + @Override + public void soneUnlocked(Sone sone) { + lockedSonesNotification.remove(sone); + Ticker.getInstance().deregisterEvent(lockedSonesTickerObjects.remove(sone)); + } + + /** + * {@inheritDoc} + */ + @Override + public void soneInserting(Sone sone) { + TemplateNotification soneInsertNotification = getSoneInsertNotification(sone); + soneInsertNotification.set("soneStatus", "inserting"); + if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + notificationManager.addNotification(soneInsertNotification); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void soneInserted(Sone sone, long insertDuration) { + TemplateNotification soneInsertNotification = getSoneInsertNotification(sone); + soneInsertNotification.set("soneStatus", "inserted"); + soneInsertNotification.set("insertDuration", insertDuration / 1000); + if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + notificationManager.addNotification(soneInsertNotification); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void soneInsertAborted(Sone sone, Throwable cause) { + TemplateNotification soneInsertNotification = getSoneInsertNotification(sone); + soneInsertNotification.set("soneStatus", "insert-aborted"); + soneInsertNotification.set("insert-error", cause); + if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) { + notificationManager.addNotification(soneInsertNotification); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void updateFound(Version version, long releaseTime, long latestEdition) { + newVersionNotification.getTemplateContext().set("latestVersion", version); + newVersionNotification.getTemplateContext().set("latestEdition", latestEdition); + newVersionNotification.getTemplateContext().set("releaseTime", releaseTime); + notificationManager.addNotification(newVersionNotification); + } + + /** + * {@inheritDoc} + */ + @Override + public void imageInsertStarted(Image image) { + insertingImagesNotification.add(image); + notificationManager.addNotification(insertingImagesNotification); + } + + /** + * {@inheritDoc} + */ + @Override + public void imageInsertAborted(Image image) { + insertingImagesNotification.remove(image); + } + + /** + * {@inheritDoc} + */ + @Override + public void imageInsertFinished(Image image) { + insertingImagesNotification.remove(image); + insertedImagesNotification.add(image); + notificationManager.addNotification(insertedImagesNotification); + } + + /** + * {@inheritDoc} + */ + @Override + public void imageInsertFailed(Image image, Throwable cause) { + insertingImagesNotification.remove(image); + imageInsertFailedNotification.add(image); + notificationManager.addNotification(imageInsertFailedNotification); + } + + /** + * Template provider implementation that uses + * {@link WebInterface#createReader(String)} to load templates for + * inclusion. * * @author David ‘Bombe’ Roden */ - public class AlwaysEnabledCallback implements LinkEnabledCallback { + private class ClassPathTemplateProvider implements Provider { + + /** Cache for templates. */ + private final Cache templateCache = new MemoryCache(new ValueRetriever() { + + @Override + @SuppressWarnings("synthetic-access") + public CacheItem