Move web pages to their own package
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 8 Apr 2017 13:12:20 +0000 (15:12 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 8 Apr 2017 13:22:58 +0000 (15:22 +0200)
184 files changed:
src/main/java/net/pterodactylus/sone/main/DebugLoaders.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.kt [deleted file]
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.kt [deleted file]
src/main/java/net/pterodactylus/sone/web/EditImagePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/GetImagePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/IndexPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/LikePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/LockSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/LogoutPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/NewPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/OptionsPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ReloadingPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/RescuePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/SearchPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/TrustPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UnlikePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UntrustPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/pages/EditImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/EditProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/EditProfilePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/FollowSonePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/GetImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/ImageBrowserPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/IndexPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/KnownSonesPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/LikePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/LockSonePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/LogoutPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/MarkAsKnownPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/NewPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/OptionsPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/ReloadingPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/RescuePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/SearchPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/SoneTemplatePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/TrustPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UnbookmarkPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UnfollowSonePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UnlikePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UnlockSonePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UntrustPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/UploadImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/ViewPostPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/pages/ViewSonePage.java [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/AboutPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/BookmarkPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/BookmarksPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/CreatePostPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/CreateSonePage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeleteAlbumPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeleteImagePage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeletePostPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeleteReplyPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DeleteSonePage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DismissNotificationPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/DistrustPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/EditAlbumPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/LoginPage.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/web/pages/AboutPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarksPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.kt [deleted file]
src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.kt [deleted file]
src/test/java/net/pterodactylus/sone/web/NewPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/WebPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/pages/NewPageTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/web/pages/WebPageTest.java [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/AboutPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/BookmarkPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/BookmarksPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/CreatePostPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/CreateSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeleteAlbumPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeleteImagePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeletePostPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeleteReplyPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DeleteSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DismissNotificationPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/DistrustPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/EditAlbumPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/EditImagePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/EditProfileFieldPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/EditProfilePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/FollowSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/GetImagePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/ImageBrowserPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/IndexPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/KnownSonesPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/LikePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/LockSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/LoginPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/LogoutPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/MarkAsKnownPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/OptionsPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/ReloadingPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/RescuePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/SearchPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/TrustPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UnbookmarkPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UnfollowSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UnlikePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UnlockSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UntrustPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/UploadImagePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/ViewPostPageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/ViewSonePageTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/pages/AboutPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarkPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarksPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/CreateSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeletePostPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/GetImagePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/LikePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/LockSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/LoginPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/LogoutPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/ReloadingPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/RescuePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/SearchPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UnlikePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/UploadImagePageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/ViewPostPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/ViewSonePageTest.kt [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/web/image.png [deleted file]
src/test/resources/net/pterodactylus/sone/web/no-image.png [deleted file]
src/test/resources/net/pterodactylus/sone/web/pages/image.png [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/web/pages/no-image.png [new file with mode: 0644]

index 11755e0..d0168ae 100644 (file)
@@ -3,7 +3,7 @@ package net.pterodactylus.sone.main;
 import java.io.File;
 
 import net.pterodactylus.sone.template.FilesystemTemplate;
 import java.io.File;
 
 import net.pterodactylus.sone.template.FilesystemTemplate;
-import net.pterodactylus.sone.web.ReloadingPage;
+import net.pterodactylus.sone.web.pages.ReloadingPage;
 import net.pterodactylus.util.template.FilesystemTemplateProvider;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.template.FilesystemTemplateProvider;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.kt b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.kt
deleted file mode 100644 (file)
index e91ad37..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
-import net.pterodactylus.sone.text.TextFilter
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user create a new album.
- */
-class CreateAlbumPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("createAlbum.html", template, "Page.CreateAlbum.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               if (request.isPOST) {
-                       val name = request.httpRequest.getPartAsStringFailsafe("name", 64).trim()
-                       if (name.isEmpty()) {
-                               templateContext["nameMissing"] = true
-                               return
-                       }
-                       val description = request.httpRequest.getPartAsStringFailsafe("description", 256).trim()
-                       val currentSone = webInterface.getCurrentSoneCreatingSession(request.toadletContext)
-                       val parentId = request.httpRequest.getPartAsStringFailsafe("parent", 36)
-                       val parent = if (parentId == "") currentSone.rootAlbum else webInterface.core.getAlbum(parentId)
-                       val album = webInterface.core.createAlbum(currentSone, parent)
-                       try {
-                               album.modify().apply {
-                                       setTitle(name)
-                                       setDescription(TextFilter.filter(request.httpRequest.getHeader("Host"), description))
-                               }.update()
-                       } catch (e: AlbumTitleMustNotBeEmpty) {
-                               throw RedirectException("emptyAlbumTitle.html")
-                       }
-                       webInterface.core.touchConfiguration()
-                       throw RedirectException("imageBrowser.html?album=${album.id}")
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.kt b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.kt
deleted file mode 100644 (file)
index 43fbeaf..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.text.TextFilter
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * This page lets the user post a reply to a post.
- */
-class CreateReplyPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("createReply.html", template, "Page.CreateReply.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val postId = request.httpRequest.getPartAsStringFailsafe("post", 36).apply { templateContext["postId"] = this }
-               val text = request.httpRequest.getPartAsStringFailsafe("text", 65536).trim().apply { templateContext["text"] = this }
-               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256).apply { templateContext["returnPage"] = this }
-               if (request.isPOST) {
-                       if (text == "") {
-                               templateContext["errorTextEmpty"] = true
-                               return
-                       }
-                       val post = webInterface.core.getPost(postId).orNull() ?: throw RedirectException("noPermission.html")
-                       val sender = webInterface.core.getLocalSone(request.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(request.toadletContext)
-                       webInterface.core.createReply(sender, post, TextFilter.filter(request.httpRequest.getHeader("Host"), text))
-                       throw RedirectException(returnPage)
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditImagePage.java b/src/main/java/net/pterodactylus/sone/web/EditImagePage.java
deleted file mode 100644 (file)
index 419bc1d..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Sone - EditImagePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user edit title and description of an {@link Image}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditImagePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “edit image” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditImagePage(Template template, WebInterface webInterface) {
-               super("editImage.html", template, "Page.EditImage.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Image image = webInterface.getCore().getImage(imageId, false);
-                       if (image == null) {
-                               throw new RedirectException("invalid.html");
-                       }
-                       if (!image.getSone().isLocal()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
-                               image.getAlbum().moveImageUp(image);
-                       } else if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
-                               image.getAlbum().moveImageDown(image);
-                       } else {
-                               String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
-                               String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1024).trim();
-                               if (title.length() == 0) {
-                                       throw new RedirectException("emptyImageTitle.html");
-                               }
-                               image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
-                       }
-                       webInterface.getCore().touchConfiguration();
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
deleted file mode 100644 (file)
index bbc6d6b..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Sone - EditProfileFieldPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user edit the name of a profile field.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditProfileFieldPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “edit profile field” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditProfileFieldPage(Template template, WebInterface webInterface) {
-               super("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               Profile profile = currentSone.getProfile();
-
-               /* get parameters from request. */
-               String fieldId = request.getHttpRequest().getParam("field");
-               Field field = profile.getFieldById(fieldId);
-               if (field == null) {
-                       throw new RedirectException("invalid.html");
-               }
-
-               /* process the POST request. */
-               if (request.getMethod() == Method.POST) {
-                       if (request.getHttpRequest().getPartAsStringFailsafe("cancel", 4).equals("true")) {
-                               throw new RedirectException("editProfile.html#profile-fields");
-                       }
-                       fieldId = request.getHttpRequest().getPartAsStringFailsafe("field", 36);
-                       field = profile.getFieldById(fieldId);
-                       if (field == null) {
-                               throw new RedirectException("invalid.html");
-                       }
-                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 256);
-                       Field existingField = profile.getFieldByName(name);
-                       if ((existingField != null) && (!field.equals(existingField))) {
-                               templateContext.set("duplicateFieldName", true);
-                       } else {
-                               if (!name.equals(field.getName())) {
-                                       field.setName(name);
-                                       currentSone.setProfile(profile);
-                               }
-                               throw new RedirectException("editProfile.html#profile-fields");
-                       }
-               }
-
-               /* store current values in template. */
-               templateContext.set("field", field);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java b/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
deleted file mode 100644 (file)
index cffdeb9..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Sone - EditProfilePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.text.TextFilter.filter;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.List;
-
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.DuplicateField;
-import net.pterodactylus.sone.data.Profile.Field;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-import freenet.clients.http.ToadletContext;
-
-/**
- * This page lets the user edit her profile.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditProfilePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “edit profile” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditProfilePage(Template template, WebInterface webInterface) {
-               super("editProfile.html", template, "Page.EditProfile.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               ToadletContext toadletContenxt = request.getToadletContext();
-               Sone currentSone = getCurrentSone(toadletContenxt);
-               Profile profile = currentSone.getProfile();
-               String firstName = profile.getFirstName();
-               String middleName = profile.getMiddleName();
-               String lastName = profile.getLastName();
-               Integer birthDay = profile.getBirthDay();
-               Integer birthMonth = profile.getBirthMonth();
-               Integer birthYear = profile.getBirthYear();
-               String avatarId = profile.getAvatar();
-               List<Field> fields = profile.getFields();
-               if (request.getMethod() == Method.POST) {
-                       if (request.getHttpRequest().getPartAsStringFailsafe("save-profile", 4).equals("true")) {
-                               firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
-                               middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
-                               lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
-                               birthDay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim(), null);
-                               birthMonth = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim(), null);
-                               birthYear = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim(), null);
-                               avatarId = request.getHttpRequest().getPartAsStringFailsafe("avatarId", 36);
-                               profile.setFirstName(firstName.length() > 0 ? firstName : null);
-                               profile.setMiddleName(middleName.length() > 0 ? middleName : null);
-                               profile.setLastName(lastName.length() > 0 ? lastName : null);
-                               profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
-                               profile.setAvatar(webInterface.getCore().getImage(avatarId, false));
-                               for (Field field : fields) {
-                                       String value = request.getHttpRequest().getPartAsStringFailsafe("field-" + field.getId(), 400);
-                                       String filteredValue = filter(request.getHttpRequest().getHeader("Host"), value);
-                                       field.setValue(filteredValue);
-                               }
-                               currentSone.setProfile(profile);
-                               webInterface.getCore().touchConfiguration();
-                               throw new RedirectException("editProfile.html");
-                       } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
-                               String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
-                               try {
-                                       profile.addField(fieldName);
-                                       currentSone.setProfile(profile);
-                                       webInterface.getCore().touchConfiguration();
-                                       throw new RedirectException("editProfile.html#profile-fields");
-                               } catch (DuplicateField df1) {
-                                       templateContext.set("fieldName", fieldName);
-                                       templateContext.set("duplicateFieldName", true);
-                               }
-                       } else {
-                               String id = getFieldId(request, "delete-field-");
-                               if (id != null) {
-                                       throw new RedirectException("deleteProfileField.html?field=" + id);
-                               }
-                               id = getFieldId(request, "move-up-field-");
-                               if (id != null) {
-                                       Field field = profile.getFieldById(id);
-                                       if (field == null) {
-                                               throw new RedirectException("invalid.html");
-                                       }
-                                       profile.moveFieldUp(field);
-                                       currentSone.setProfile(profile);
-                                       throw new RedirectException("editProfile.html#profile-fields");
-                               }
-                               id = getFieldId(request, "move-down-field-");
-                               if (id != null) {
-                                       Field field = profile.getFieldById(id);
-                                       if (field == null) {
-                                               throw new RedirectException("invalid.html");
-                                       }
-                                       profile.moveFieldDown(field);
-                                       currentSone.setProfile(profile);
-                                       throw new RedirectException("editProfile.html#profile-fields");
-                               }
-                               id = getFieldId(request, "edit-field-");
-                               if (id != null) {
-                                       throw new RedirectException("editProfileField.html?field=" + id);
-                               }
-                       }
-               }
-               templateContext.set("firstName", firstName);
-               templateContext.set("middleName", middleName);
-               templateContext.set("lastName", lastName);
-               templateContext.set("birthDay", birthDay);
-               templateContext.set("birthMonth", birthMonth);
-               templateContext.set("birthYear", birthYear);
-               templateContext.set("avatarId", avatarId);
-               templateContext.set("fields", fields);
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Searches for a part whose names starts with the given {@code String} and
-        * extracts the ID from the located name.
-        *
-        * @param request
-        *            The request to get the parts from
-        * @param partNameStart
-        *            The start of the name of the requested part
-        * @return The parsed ID, or {@code null} if there was no part matching the
-        *         given string
-        */
-       private static String getFieldId(FreenetRequest request, String partNameStart) {
-               for (String partName : request.getHttpRequest().getParts()) {
-                       if (partName.startsWith(partNameStart)) {
-                               return partName.substring(partNameStart.length());
-                       }
-               }
-               return null;
-       }
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
deleted file mode 100644 (file)
index 0652a52..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Sone - FollowSonePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * This page lets the user follow another Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FollowSonePage extends SoneTemplatePage {
-
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public FollowSonePage(Template template, WebInterface webInterface) {
-               super("followSone.html", template, "Page.FollowSone.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
-                       for (String soneId : soneIds.split("[ ,]+")) {
-                               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-                               if (sone.isPresent()) {
-                                       webInterface.getCore().followSone(currentSone, soneId);
-                                       webInterface.getCore().markSoneKnown(sone.get());
-                               }
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/GetImagePage.java b/src/main/java/net/pterodactylus/sone/web/GetImagePage.java
deleted file mode 100644 (file)
index ec38d0a..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Sone - GetImagePage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.io.IOException;
-import java.net.URI;
-
-import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.web.page.FreenetPage;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.web.Response;
-
-/**
- * Page that delivers a {@link TemporaryImage} to the browser.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetImagePage implements FreenetPage {
-
-       /** The Sone web interface. */
-       private final WebInterface webInterface;
-
-       /**
-        * Creates a new “get image” page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetImagePage(WebInterface webInterface) {
-               this.webInterface = webInterface;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return "getImage.html";
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isPrefixPage() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
-               String imageId = request.getHttpRequest().getParam("image");
-               TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
-               if (temporaryImage == null) {
-                       return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
-               }
-               String contentType= temporaryImage.getMimeType();
-               return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java b/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
deleted file mode 100644 (file)
index 7a222d2..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Sone - ImageBrowserPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static com.google.common.collect.FluentIterable.from;
-import static net.pterodactylus.sone.data.Album.FLATTENER;
-import static net.pterodactylus.sone.data.Album.NOT_EMPTY;
-import static net.pterodactylus.sone.data.Album.TITLE_COMPARATOR;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * The image browser page is the entry page for the image management.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ImageBrowserPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new image browser page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public ImageBrowserPage(Template template, WebInterface webInterface) {
-               super("imageBrowser.html", template, "Page.ImageBrowser.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String albumId = request.getHttpRequest().getParam("album", null);
-               if (albumId != null) {
-                       Album album = webInterface.getCore().getAlbum(albumId);
-                       templateContext.set("albumRequested", true);
-                       templateContext.set("album", album);
-                       templateContext.set("page", request.getHttpRequest().getParam("page"));
-                       return;
-               }
-               String imageId = request.getHttpRequest().getParam("image", null);
-               if (imageId != null) {
-                       Image image = webInterface.getCore().getImage(imageId, false);
-                       templateContext.set("imageRequested", true);
-                       templateContext.set("image", image);
-                       return;
-               }
-               String soneId = request.getHttpRequest().getParam("sone", null);
-               if (soneId != null) {
-                       Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-                       templateContext.set("soneRequested", true);
-                       templateContext.set("sone", sone.orNull());
-                       return;
-               }
-               String mode = request.getHttpRequest().getParam("mode", null);
-               if ("gallery".equals(mode)) {
-                       templateContext.set("galleryRequested", true);
-                       List<Album> albums = new ArrayList<Album>();
-                       for (Sone sone : webInterface.getCore().getSones()) {
-                               albums.addAll(from(sone.getRootAlbum().getAlbums()).transformAndConcat(FLATTENER).filter(NOT_EMPTY).toList());
-                       }
-                       Collections.sort(albums, TITLE_COMPARATOR);
-                       Pagination<Album> albumPagination = new Pagination<Album>(albums, 12).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
-                       templateContext.set("albumPagination", albumPagination);
-                       templateContext.set("albums", albumPagination.getItems());
-                       return;
-               }
-               Sone sone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
-               templateContext.set("soneRequested", true);
-               templateContext.set("sone", sone);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return true;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/IndexPage.java b/src/main/java/net/pterodactylus/sone/web/IndexPage.java
deleted file mode 100644 (file)
index 1d40f45..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Sone - IndexPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.PostVisibilityFilter;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.Collections2;
-
-/**
- * The index page shows the main page of Sone. This page will contain the posts
- * of all friends of the current user.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class IndexPage extends SoneTemplatePage {
-
-       private final PostVisibilityFilter postVisibilityFilter;
-
-       public IndexPage(Template template, WebInterface webInterface, PostVisibilityFilter postVisibilityFilter) {
-               super("index.html", template, "Page.Index.Title", webInterface, true);
-               this.postVisibilityFilter = postVisibilityFilter;
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               final Sone currentSone = getCurrentSone(request.getToadletContext());
-               Collection<Post> allPosts = new ArrayList<Post>();
-               allPosts.addAll(currentSone.getPosts());
-               for (String friendSoneId : currentSone.getFriends()) {
-                       Optional<Sone> friendSone = webInterface.getCore().getSone(friendSoneId);
-                       if (!friendSone.isPresent()) {
-                               continue;
-                       }
-                       allPosts.addAll(friendSone.get().getPosts());
-               }
-               for (Sone sone : webInterface.getCore().getSones()) {
-                       for (Post post : sone.getPosts()) {
-                               if (currentSone.equals(post.getRecipient().orNull()) && !allPosts.contains(post)) {
-                                       allPosts.add(post);
-                               }
-                       }
-               }
-               allPosts = Collections2.filter(allPosts, postVisibilityFilter.isVisible(currentSone));
-               List<Post> sortedPosts = new ArrayList<Post>(allPosts);
-               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
-               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
-               templateContext.set("pagination", pagination);
-               templateContext.set("posts", pagination.getItems());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
deleted file mode 100644 (file)
index c74f4b9..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Sone - KnownSonesPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.Ordering;
-
-/**
- * This page shows all known Sones.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class KnownSonesPage extends SoneTemplatePage {
-
-       private static final String defaultSortField = "activity";
-       private static final String defaultSortOrder = "desc";
-
-       /**
-        * Creates a “known Sones” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public KnownSonesPage(Template template, WebInterface webInterface) {
-               super("knownSones.html", template, "Page.KnownSones.Title", webInterface, false);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String sortField = request.getHttpRequest().getParam("sort", defaultSortField);
-               String sortOrder = request.getHttpRequest().getParam("order", defaultSortOrder);
-               String filter = request.getHttpRequest().getParam("filter");
-               templateContext.set("sort", sortField);
-               templateContext.set("order", sortOrder);
-               templateContext.set("filter", filter);
-               final Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
-               Collection<Sone> knownSones = Collections2.filter(webInterface.getCore().getSones(), Sone.EMPTY_SONE_FILTER);
-               if ((currentSone != null) && "followed".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
-
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return currentSone.hasFriend(sone.getId());
-                               }
-                       });
-               } else if ((currentSone != null) && "not-followed".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
-
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return !currentSone.hasFriend(sone.getId());
-                               }
-                       });
-               } else if ("new".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
-
-                               /**
-                                * {@inheritDoc}
-                                */
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return !sone.isKnown();
-                               }
-                       });
-               } else if ("not-new".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
-
-                               /**
-                                * {@inheritDoc}
-                                */
-                               @Override
-                               public boolean apply(Sone sone) {
-                                       return sone.isKnown();
-                               }
-                       });
-               } else if ("own".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, Sone.LOCAL_SONE_FILTER);
-               } else if ("not-own".equals(filter)) {
-                       knownSones = Collections2.filter(knownSones, Predicates.not(Sone.LOCAL_SONE_FILTER));
-               }
-               List<Sone> sortedSones = new ArrayList<Sone>(knownSones);
-               if ("activity".equals(sortField)) {
-                       if ("asc".equals(sortOrder)) {
-                               Collections.sort(sortedSones, Ordering.from(Sone.LAST_ACTIVITY_COMPARATOR).reverse());
-                       } else {
-                               Collections.sort(sortedSones, Sone.LAST_ACTIVITY_COMPARATOR);
-                       }
-               } else if ("posts".equals(sortField)) {
-                       if ("asc".equals(sortOrder)) {
-                               Collections.sort(sortedSones, Ordering.from(Sone.POST_COUNT_COMPARATOR).reverse());
-                       } else {
-                               Collections.sort(sortedSones, Sone.POST_COUNT_COMPARATOR);
-                       }
-               } else if ("images".equals(sortField)) {
-                       if ("asc".equals(sortOrder)) {
-                               Collections.sort(sortedSones, Ordering.from(Sone.IMAGE_COUNT_COMPARATOR).reverse());
-                       } else {
-                               Collections.sort(sortedSones, Sone.IMAGE_COUNT_COMPARATOR);
-                       }
-               } else {
-                       if ("desc".equals(sortOrder)) {
-                               Collections.sort(sortedSones, Ordering.from(Sone.NICE_NAME_COMPARATOR).reverse());
-                       } else {
-                               Collections.sort(sortedSones, Sone.NICE_NAME_COMPARATOR);
-                       }
-               }
-               Pagination<Sone> sonePagination = new Pagination<Sone>(sortedSones, 25).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
-               templateContext.set("pagination", sonePagination);
-               templateContext.set("knownSones", sonePagination.getItems());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/LikePage.java b/src/main/java/net/pterodactylus/sone/web/LikePage.java
deleted file mode 100644 (file)
index fc39eff..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Sone - LikePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user like a {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LikePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “like post” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LikePage(Template template, WebInterface webInterface) {
-               super("like.html", template, "Page.Like.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
-                       String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       if ("post".equals(type)) {
-                               currentSone.addLikedPostId(id);
-                       } else if ("reply".equals(type)) {
-                               currentSone.addLikedReplyId(id);
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/LockSonePage.java b/src/main/java/net/pterodactylus/sone/web/LockSonePage.java
deleted file mode 100644 (file)
index a0a9574..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - LockSonePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * This page lets the user lock a {@link Sone} to prevent it from being
- * inserted.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LockSonePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “lock Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LockSonePage(Template template, WebInterface webInterface) {
-               super("lockSone.html", template, "Page.LockSone.Title", webInterface);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
-               Sone sone = webInterface.getCore().getLocalSone(soneId);
-               if (sone != null) {
-                       webInterface.getCore().lockSone(sone);
-               }
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               throw new RedirectException(returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/LogoutPage.java b/src/main/java/net/pterodactylus/sone/web/LogoutPage.java
deleted file mode 100644 (file)
index 3ec62a3..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Sone - LogoutPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import freenet.clients.http.ToadletContext;
-
-/**
- * Logs a user out.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LogoutPage extends SoneTemplatePage {
-
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LogoutPage(Template template, WebInterface webInterface) {
-               super("logout.html", template, "Page.Logout.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               setCurrentSone(request.getToadletContext(), null);
-               throw new RedirectException("index.html");
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isEnabled(ToadletContext toadletContext) {
-               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
-                       return false;
-               }
-               return (getCurrentSoneWithoutCreatingSession(toadletContext) != null) && (webInterface.getCore().getLocalSones().size() != 1);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java b/src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java
deleted file mode 100644 (file)
index 57ea00f..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Sone - MarkAsKnownPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.util.StringTokenizer;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.base.Optional;
-
-/**
- * Page that lets the user mark a number of {@link Sone}s, {@link Post}s, or
- * {@link Reply Replie}s as known.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class MarkAsKnownPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “mark as known” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public MarkAsKnownPage(Template template, WebInterface webInterface) {
-               super("markAsKnown.html", template, "Page.MarkAsKnown.Title", webInterface);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
-               if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
-                       throw new RedirectException("invalid.html");
-               }
-               String ids = request.getHttpRequest().getPartAsStringFailsafe("id", 65536);
-               for (StringTokenizer idTokenizer = new StringTokenizer(ids); idTokenizer.hasMoreTokens();) {
-                       String id = idTokenizer.nextToken();
-                       if (type.equals("post")) {
-                               Optional<Post> post = webInterface.getCore().getPost(id);
-                               if (!post.isPresent()) {
-                                       continue;
-                               }
-                               webInterface.getCore().markPostKnown(post.get());
-                       } else if (type.equals("reply")) {
-                               Optional<PostReply> reply = webInterface.getCore().getPostReply(id);
-                               if (!reply.isPresent()) {
-                                       continue;
-                               }
-                               webInterface.getCore().markReplyKnown(reply.get());
-                       } else {
-                               Optional<Sone> sone = webInterface.getCore().getSone(id);
-                               if (!sone.isPresent()) {
-                                       continue;
-                               }
-                               webInterface.getCore().markSoneKnown(sone.get());
-                       }
-               }
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               throw new RedirectException(returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/NewPage.java b/src/main/java/net/pterodactylus/sone/web/NewPage.java
deleted file mode 100644 (file)
index 9eab69a..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Sone - NewPage.java - Copyright © 2013–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.PostVisibilityFilter;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * Page that displays all new posts and replies. The posts are filtered using
- * {@link PostVisibilityFilter#isPostVisible(Sone, Post)} and sorted by time.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NewPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “new posts and replies” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public NewPage(Template template, WebInterface webInterface) {
-               super("new.html", template, "Page.New.Title", webInterface);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               /* collect new elements from notifications. */
-               Set<Post> posts = new HashSet<Post>(webInterface.getNewPosts(getCurrentSoneWithoutCreatingSession(request.getToadletContext())));
-               for (PostReply reply : webInterface.getNewReplies(getCurrentSoneWithoutCreatingSession(request.getToadletContext()))) {
-                       posts.add(reply.getPost().get());
-               }
-
-               /* filter and sort them. */
-               List<Post> sortedPosts = new ArrayList<>(posts);
-               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
-
-               /* paginate them. */
-               Pagination<Post> pagination = new Pagination<>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
-               templateContext.set("pagination", pagination);
-               templateContext.set("posts", pagination.getItems());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java
deleted file mode 100644 (file)
index 7cf7246..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Sone - OptionsPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import net.pterodactylus.sone.core.Preferences;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * This page lets the user edit the options of the Sone plugin.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class OptionsPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new options page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public OptionsPage(Template template, WebInterface webInterface) {
-               super("options.html", template, "Page.Options.Title", webInterface, false);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               Preferences preferences = webInterface.getCore().getPreferences();
-               Sone currentSone = webInterface.getCurrentSoneWithoutCreatingSession(request.getToadletContext());
-               if (request.getMethod() == Method.POST) {
-                       List<String> fieldErrors = new ArrayList<String>();
-                       if (currentSone != null) {
-                               boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
-                               currentSone.getOptions().setAutoFollow(autoFollow);
-                               boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
-                               currentSone.getOptions().setSoneInsertNotificationEnabled(enableSoneInsertNotifications);
-                               boolean showNotificationNewSones = request.getHttpRequest().isPartSet("show-notification-new-sones");
-                               currentSone.getOptions().setShowNewSoneNotifications(showNotificationNewSones);
-                               boolean showNotificationNewPosts = request.getHttpRequest().isPartSet("show-notification-new-posts");
-                               currentSone.getOptions().setShowNewPostNotifications(showNotificationNewPosts);
-                               boolean showNotificationNewReplies = request.getHttpRequest().isPartSet("show-notification-new-replies");
-                               currentSone.getOptions().setShowNewReplyNotifications(showNotificationNewReplies);
-                               String showCustomAvatars = request.getHttpRequest().getPartAsStringFailsafe("show-custom-avatars", 32);
-                               currentSone.getOptions().setShowCustomAvatars(LoadExternalContent.valueOf(showCustomAvatars));
-                               String loadLinkedImages = request.getHttpRequest().getPartAsStringFailsafe("load-linked-images", 32);
-                               currentSone.getOptions().setLoadLinkedImages(LoadExternalContent.valueOf(loadLinkedImages));
-                               webInterface.getCore().touchConfiguration();
-                       }
-                       Integer insertionDelay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16), null);
-                       if (!preferences.validateInsertionDelay(insertionDelay)) {
-                               fieldErrors.add("insertion-delay");
-                       } else {
-                               preferences.setInsertionDelay(insertionDelay);
-                       }
-                       Integer postsPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
-                       if (!preferences.validatePostsPerPage(postsPerPage)) {
-                               fieldErrors.add("posts-per-page");
-                       } else {
-                               preferences.setPostsPerPage(postsPerPage);
-                       }
-                       Integer imagesPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("images-per-page", 4), null);
-                       if (!preferences.validateImagesPerPage(imagesPerPage)) {
-                               fieldErrors.add("images-per-page");
-                       } else {
-                               preferences.setImagesPerPage(imagesPerPage);
-                       }
-                       Integer charactersPerPost = parseInt(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null);
-                       if (!preferences.validateCharactersPerPost(charactersPerPost)) {
-                               fieldErrors.add("characters-per-post");
-                       } else {
-                               preferences.setCharactersPerPost(charactersPerPost);
-                       }
-                       Integer postCutOffLength = parseInt(request.getHttpRequest().getPartAsStringFailsafe("post-cut-off-length", 10), null);
-                       if (!preferences.validatePostCutOffLength(postCutOffLength)) {
-                               fieldErrors.add("post-cut-off-length");
-                       } else {
-                               preferences.setPostCutOffLength(postCutOffLength);
-                       }
-                       boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
-                       preferences.setRequireFullAccess(requireFullAccess);
-                       Integer positiveTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3), null);
-                       if (!preferences.validatePositiveTrust(positiveTrust)) {
-                               fieldErrors.add("positive-trust");
-                       } else {
-                               preferences.setPositiveTrust(positiveTrust);
-                       }
-                       Integer negativeTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4), null);
-                       if (!preferences.validateNegativeTrust(negativeTrust)) {
-                               fieldErrors.add("negative-trust");
-                       } else {
-                               preferences.setNegativeTrust(negativeTrust);
-                       }
-                       String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
-                       if (trustComment.trim().length() == 0) {
-                               trustComment = null;
-                       }
-                       preferences.setTrustComment(trustComment);
-                       boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active");
-                       preferences.setFcpInterfaceActive(fcpInterfaceActive);
-                       Integer fcpFullAccessRequiredInteger = parseInt(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
-                       FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
-                       preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
-                       webInterface.getCore().touchConfiguration();
-                       if (fieldErrors.isEmpty()) {
-                               throw new RedirectException(getPath());
-                       }
-                       templateContext.set("fieldErrors", fieldErrors);
-               }
-               if (currentSone != null) {
-                       templateContext.set("auto-follow", currentSone.getOptions().isAutoFollow());
-                       templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().isSoneInsertNotificationEnabled());
-                       templateContext.set("show-notification-new-sones", currentSone.getOptions().isShowNewSoneNotifications());
-                       templateContext.set("show-notification-new-posts", currentSone.getOptions().isShowNewPostNotifications());
-                       templateContext.set("show-notification-new-replies", currentSone.getOptions().isShowNewReplyNotifications());
-                       templateContext.set("show-custom-avatars", currentSone.getOptions().getShowCustomAvatars().name());
-                       templateContext.set("load-linked-images", currentSone.getOptions().getLoadLinkedImages().name());
-               }
-               templateContext.set("insertion-delay", preferences.getInsertionDelay());
-               templateContext.set("posts-per-page", preferences.getPostsPerPage());
-               templateContext.set("images-per-page", preferences.getImagesPerPage());
-               templateContext.set("characters-per-post", preferences.getCharactersPerPost());
-               templateContext.set("post-cut-off-length", preferences.getPostCutOffLength());
-               templateContext.set("require-full-access", preferences.isRequireFullAccess());
-               templateContext.set("positive-trust", preferences.getPositiveTrust());
-               templateContext.set("negative-trust", preferences.getNegativeTrust());
-               templateContext.set("trust-comment", preferences.getTrustComment());
-               templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
-               templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ReloadingPage.java b/src/main/java/net/pterodactylus/sone/web/ReloadingPage.java
deleted file mode 100644 (file)
index bc18816..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Sone - ReloadingPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import net.pterodactylus.util.io.StreamCopier;
-import net.pterodactylus.util.web.Page;
-import net.pterodactylus.util.web.Request;
-import net.pterodactylus.util.web.Response;
-
-/**
- * {@link Page} implementation that delivers static files from the filesystem.
- *
- * @param <REQ>
- *            The type of the request
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ReloadingPage<REQ extends Request> implements Page<REQ> {
-
-       private final String pathPrefix;
-       private final String filesystemPath;
-       private final String mimeType;
-
-       public ReloadingPage(String pathPrefix, String filesystemPathPrefix, String mimeType) {
-               this.pathPrefix = pathPrefix;
-               this.filesystemPath = filesystemPathPrefix;
-               this.mimeType = mimeType;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return pathPrefix;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isPrefixPage() {
-               return true;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(REQ request, Response response) throws IOException {
-               String path = request.getUri().getPath();
-               int lastSlash = path.lastIndexOf('/');
-               String filename = path.substring(lastSlash + 1);
-               try (InputStream fileInputStream = new FileInputStream(new File(filesystemPath, filename));
-                       OutputStream contentOutputStream = response.getContent()) {
-                       StreamCopier.copy(fileInputStream, contentOutputStream);
-               } catch (FileNotFoundException fnfe1) {
-                       return response.setStatusCode(404).setStatusText("Not found.");
-               }
-               return response.setStatusCode(200).setStatusText("OK").setContentType(mimeType);
-       }
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/RescuePage.java b/src/main/java/net/pterodactylus/sone/web/RescuePage.java
deleted file mode 100644 (file)
index 0f94563..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Sone - RescuePage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseLong;
-
-import net.pterodactylus.sone.core.SoneRescuer;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user control the rescue mode for a Sone.
- *
- * @see SoneRescuer
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class RescuePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new rescue page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public RescuePage(Template template, WebInterface webInterface) {
-               super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
-               SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
-               if (request.getMethod() == Method.POST) {
-                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
-                               long edition = parseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
-                               if (edition > -1) {
-                                       soneRescuer.setEdition(edition);
-                               }
-                               soneRescuer.startNextFetch();
-                       }
-                       throw new RedirectException("rescue.html");
-               }
-               templateContext.set("soneRescuer", soneRescuer);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/SearchPage.java
deleted file mode 100644 (file)
index 8b444a7..0000000
+++ /dev/null
@@ -1,658 +0,0 @@
-/*
- * Sone - SearchPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static com.google.common.base.Optional.fromNullable;
-import static com.google.common.primitives.Ints.tryParse;
-import static java.util.logging.Logger.getLogger;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
-import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.text.StringEscaper;
-import net.pterodactylus.util.text.TextException;
-
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Ordering;
-
-/**
- * This page lets the user search for posts and replies that contain certain
- * words.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class SearchPage extends SoneTemplatePage {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(SearchPage.class.getName());
-
-       /** Short-term cache. */
-       private final LoadingCache<List<Phrase>, Set<Hit<Post>>> hitCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<List<Phrase>, Set<Hit<Post>>>() {
-
-               @Override
-               @SuppressWarnings("synthetic-access")
-               public Set<Hit<Post>> load(List<Phrase> phrases) {
-                       Set<Post> posts = new HashSet<Post>();
-                       for (Sone sone : webInterface.getCore().getSones()) {
-                               posts.addAll(sone.getPosts());
-                       }
-                       return getHits(Collections2.filter(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
-               }
-       });
-
-       /**
-        * Creates a new search page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public SearchPage(Template template, WebInterface webInterface) {
-               super("search.html", template, "Page.Search.Title", webInterface);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       @SuppressWarnings("synthetic-access")
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String query = request.getHttpRequest().getParam("query").trim();
-               if (query.length() == 0) {
-                       throw new RedirectException("index.html");
-               }
-
-               List<Phrase> phrases = parseSearchPhrases(query);
-               if (phrases.isEmpty()) {
-                       throw new RedirectException("index.html");
-               }
-
-               /* check for a couple of shortcuts. */
-               if (phrases.size() == 1) {
-                       String phrase = phrases.get(0).getPhrase();
-
-                       /* is it a Sone ID? */
-                       redirectIfNotNull(getSoneId(phrase), "viewSone.html?sone=");
-
-                       /* is it a post ID? */
-                       redirectIfNotNull(getPostId(phrase), "viewPost.html?post=");
-
-                       /* is it a reply ID? show the post. */
-                       redirectIfNotNull(getReplyPostId(phrase), "viewPost.html?post=");
-
-                       /* is it an album ID? */
-                       redirectIfNotNull(getAlbumId(phrase), "imageBrowser.html?album=");
-
-                       /* is it an image ID? */
-                       redirectIfNotNull(getImageId(phrase), "imageBrowser.html?image=");
-               }
-
-               Collection<Sone> sones = webInterface.getCore().getSones();
-               Collection<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
-
-               Collection<Hit<Post>> postHits = hitCache.getUnchecked(phrases);
-
-               /* now filter. */
-               soneHits = Collections2.filter(soneHits, Hit.POSITIVE_FILTER);
-               postHits = Collections2.filter(postHits, Hit.POSITIVE_FILTER);
-
-               /* now sort. */
-               List<Hit<Sone>> sortedSoneHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(soneHits);
-               List<Hit<Post>> sortedPostHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(postHits);
-
-               /* extract Sones and posts. */
-               List<Sone> resultSones = FluentIterable.from(sortedSoneHits).transform(new HitMapper<Sone>()).toList();
-               List<Post> resultPosts = FluentIterable.from(sortedPostHits).transform(new HitMapper<Post>()).toList();
-
-               /* pagination. */
-               Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("sonePage"))).or(0));
-               Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("postPage"))).or(0));
-
-               templateContext.set("sonePagination", sonePagination);
-               templateContext.set("soneHits", sonePagination.getItems());
-               templateContext.set("postPagination", postPagination);
-               templateContext.set("postHits", postPagination.getItems());
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Collects hit information for the given objects. The objects are converted
-        * to a {@link String} using the given {@link StringGenerator}, and the
-        * {@link #calculateScore(List, String) calculated score} is stored together
-        * with the object in a {@link Hit}, and all resulting {@link Hit}s are then
-        * returned.
-        *
-        * @param <T>
-        *            The type of the objects
-        * @param objects
-        *            The objects to search over
-        * @param phrases
-        *            The phrases to search for
-        * @param stringGenerator
-        *            The string generator for the objects
-        * @return The hits for the given phrases
-        */
-       private static <T> Set<Hit<T>> getHits(Collection<T> objects, List<Phrase> phrases, StringGenerator<T> stringGenerator) {
-               Set<Hit<T>> hits = new HashSet<Hit<T>>();
-               for (T object : objects) {
-                       String objectString = stringGenerator.generateString(object);
-                       double score = calculateScore(phrases, objectString);
-                       hits.add(new Hit<T>(object, score));
-               }
-               return hits;
-       }
-
-       /**
-        * Parses the given query into search phrases. The query is split on
-        * whitespace while allowing to group words using single or double quotes.
-        * Isolated phrases starting with a “+” are
-        * {@link Phrase.Optionality#REQUIRED}, phrases with a “-” are
-        * {@link Phrase.Optionality#FORBIDDEN}.
-        *
-        * @param query
-        *            The query to parse
-        * @return The parsed phrases
-        */
-       private static List<Phrase> parseSearchPhrases(String query) {
-               List<String> parsedPhrases;
-               try {
-                       parsedPhrases = StringEscaper.parseLine(query);
-               } catch (TextException te1) {
-                       /* invalid query. */
-                       return Collections.emptyList();
-               }
-
-               List<Phrase> phrases = new ArrayList<Phrase>();
-               for (String phrase : parsedPhrases) {
-                       if (phrase.startsWith("+")) {
-                               if (phrase.length() > 1) {
-                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
-                               } else {
-                                       phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
-                               }
-                       } else if (phrase.startsWith("-")) {
-                               if (phrase.length() > 1) {
-                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
-                               } else {
-                                       phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
-                               }
-                       } else {
-                               phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
-                       }
-               }
-               return phrases;
-       }
-
-       /**
-        * Calculates the score for the given expression when using the given
-        * phrases.
-        *
-        * @param phrases
-        *            The phrases to search for
-        * @param expression
-        *            The expression to search
-        * @return The score of the expression
-        */
-       private static double calculateScore(List<Phrase> phrases, String expression) {
-               logger.log(Level.FINEST, String.format("Calculating Score for “%s”…", expression));
-               double optionalHits = 0;
-               double requiredHits = 0;
-               int forbiddenHits = 0;
-               int requiredPhrases = 0;
-               for (Phrase phrase : phrases) {
-                       String phraseString = phrase.getPhrase().toLowerCase();
-                       if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
-                               ++requiredPhrases;
-                       }
-                       int matches = 0;
-                       int index = 0;
-                       double score = 0;
-                       while (index < expression.length()) {
-                               int position = expression.toLowerCase().indexOf(phraseString, index);
-                               if (position == -1) {
-                                       break;
-                               }
-                               score += Math.pow(1 - position / (double) expression.length(), 2);
-                               index = position + phraseString.length();
-                               logger.log(Level.FINEST, String.format("Got hit at position %d.", position));
-                               ++matches;
-                       }
-                       logger.log(Level.FINEST, String.format("Score: %f", score));
-                       if (matches == 0) {
-                               continue;
-                       }
-                       if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
-                               requiredHits += score;
-                       }
-                       if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
-                               optionalHits += score;
-                       }
-                       if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
-                               forbiddenHits += matches;
-                       }
-               }
-               return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2);
-       }
-
-       /**
-        * Throws a
-        * {@link net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException}
-        * if the given object is not {@code null}, appending the object to the
-        * given target URL.
-        *
-        * @param object
-        *            The object on which to redirect
-        * @param target
-        *            The target of the redirect
-        * @throws RedirectException
-        *             if {@code object} is not {@code null}
-        */
-       private static void redirectIfNotNull(String object, String target) throws RedirectException {
-               if (object != null) {
-                       throw new RedirectException(target + object);
-               }
-       }
-
-       /**
-        * If the given phrase contains a Sone ID (optionally prefixed by
-        * “sone://”), returns said Sone ID, otherwise return {@code null}.
-        *
-        * @param phrase
-        *            The phrase that maybe is a Sone ID
-        * @return The Sone ID, or {@code null}
-        */
-       private String getSoneId(String phrase) {
-               String soneId = phrase.startsWith("sone://") ? phrase.substring(7) : phrase;
-               return (webInterface.getCore().getSone(soneId).isPresent()) ? soneId : null;
-       }
-
-       /**
-        * If the given phrase contains a post ID (optionally prefixed by
-        * “post://”), returns said post ID, otherwise return {@code null}.
-        *
-        * @param phrase
-        *            The phrase that maybe is a post ID
-        * @return The post ID, or {@code null}
-        */
-       private String getPostId(String phrase) {
-               String postId = phrase.startsWith("post://") ? phrase.substring(7) : phrase;
-               return (webInterface.getCore().getPost(postId).isPresent()) ? postId : null;
-       }
-
-       /**
-        * If the given phrase contains a reply ID (optionally prefixed by
-        * “reply://”), returns the ID of the post the reply belongs to, otherwise
-        * return {@code null}.
-        *
-        * @param phrase
-        *            The phrase that maybe is a reply ID
-        * @return The reply’s post ID, or {@code null}
-        */
-       private String getReplyPostId(String phrase) {
-               String replyId = phrase.startsWith("reply://") ? phrase.substring(8) : phrase;
-               Optional<PostReply> postReply = webInterface.getCore().getPostReply(replyId);
-               if (!postReply.isPresent()) {
-                       return null;
-               }
-               return postReply.get().getPostId();
-       }
-
-       /**
-        * If the given phrase contains an album ID (optionally prefixed by
-        * “album://”), returns said album ID, otherwise return {@code null}.
-        *
-        * @param phrase
-        *            The phrase that maybe is an album ID
-        * @return The album ID, or {@code null}
-        */
-       private String getAlbumId(String phrase) {
-               String albumId = phrase.startsWith("album://") ? phrase.substring(8) : phrase;
-               return (webInterface.getCore().getAlbum(albumId) != null) ? albumId : null;
-       }
-
-       /**
-        * If the given phrase contains an image ID (optionally prefixed by
-        * “image://”), returns said image ID, otherwise return {@code null}.
-        *
-        * @param phrase
-        *            The phrase that maybe is an image ID
-        * @return The image ID, or {@code null}
-        */
-       private String getImageId(String phrase) {
-               String imageId = phrase.startsWith("image://") ? phrase.substring(8) : phrase;
-               return (webInterface.getCore().getImage(imageId, false) != null) ? imageId : null;
-       }
-
-       /**
-        * Converts a given object into a {@link String}.
-        *
-        * @param <T>
-        *            The type of the objects
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static interface StringGenerator<T> {
-
-               /**
-                * Generates a {@link String} for the given object.
-                *
-                * @param object
-                *            The object to generate the {@link String} for
-                * @return The generated {@link String}
-                */
-               public String generateString(T object);
-
-       }
-
-       /**
-        * Generates a {@link String} from a {@link Sone}, concatenating the name of
-        * the Sone and all {@link Profile} {@link Field} values.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static class SoneStringGenerator implements StringGenerator<Sone> {
-
-               /** A static instance of a complete Sone string generator. */
-               public static final SoneStringGenerator COMPLETE_GENERATOR = new SoneStringGenerator(true);
-
-               /**
-                * A static instance of a Sone string generator that will only use the
-                * name of the Sone.
-                */
-               public static final SoneStringGenerator NAME_GENERATOR = new SoneStringGenerator(false);
-
-               /** Whether to generate a string from all data of a Sone. */
-               private final boolean complete;
-
-               /**
-                * Creates a new Sone string generator.
-                *
-                * @param complete
-                *            {@code true} to use the profile’s fields, {@code false} to
-                *            not to use the profile‘s fields
-                */
-               private SoneStringGenerator(boolean complete) {
-                       this.complete = complete;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public String generateString(Sone sone) {
-                       StringBuilder soneString = new StringBuilder();
-                       soneString.append(sone.getName());
-                       Profile soneProfile = sone.getProfile();
-                       if (soneProfile.getFirstName() != null) {
-                               soneString.append(' ').append(soneProfile.getFirstName());
-                       }
-                       if (soneProfile.getMiddleName() != null) {
-                               soneString.append(' ').append(soneProfile.getMiddleName());
-                       }
-                       if (soneProfile.getLastName() != null) {
-                               soneString.append(' ').append(soneProfile.getLastName());
-                       }
-                       if (complete) {
-                               for (Field field : soneProfile.getFields()) {
-                                       soneString.append(' ').append(field.getValue());
-                               }
-                       }
-                       return soneString.toString();
-               }
-
-       }
-
-       /**
-        * Generates a {@link String} from a {@link Post}, concatenating the text of
-        * the post, the text of all {@link Reply}s, and the name of all
-        * {@link Sone}s that have replied.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private class PostStringGenerator implements StringGenerator<Post> {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public String generateString(Post post) {
-                       StringBuilder postString = new StringBuilder();
-                       postString.append(post.getText());
-                       if (post.getRecipient().isPresent()) {
-                               postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient().get()));
-                       }
-                       for (PostReply reply : Collections2.filter(webInterface.getCore().getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER)) {
-                               postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(reply.getSone()));
-                               postString.append(' ').append(reply.getText());
-                       }
-                       return postString.toString();
-               }
-
-       }
-
-       /**
-        * A search phrase.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static class Phrase {
-
-               /**
-                * The optionality of a search phrase.
-                *
-                * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
-                *         Roden</a>
-                */
-               public enum Optionality {
-
-                       /** The phrase is optional. */
-                       OPTIONAL,
-
-                       /** The phrase is required. */
-                       REQUIRED,
-
-                       /** The phrase is forbidden. */
-                       FORBIDDEN
-
-               }
-
-               /** The phrase to search for. */
-               private final String phrase;
-
-               /** The optionality of the phrase. */
-               private final Optionality optionality;
-
-               /**
-                * Creates a new phrase.
-                *
-                * @param phrase
-                *            The phrase to search for
-                * @param optionality
-                *            The optionality of the phrase
-                */
-               public Phrase(String phrase, Optionality optionality) {
-                       this.optionality = optionality;
-                       this.phrase = phrase;
-               }
-
-               /**
-                * Returns the phrase to search for.
-                *
-                * @return The phrase to search for
-                */
-               public String getPhrase() {
-                       return phrase;
-               }
-
-               /**
-                * Returns the optionality of the phrase.
-                *
-                * @return The optionality of the phrase
-                */
-               public Optionality getOptionality() {
-                       return optionality;
-               }
-
-               //
-               // OBJECT METHODS
-               //
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public int hashCode() {
-                       return phrase.hashCode() ^ ((optionality == Optionality.FORBIDDEN) ? (0xaaaaaaaa) : ((optionality == Optionality.REQUIRED) ? 0x55555555 : 0));
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public boolean equals(Object object) {
-                       if (!(object instanceof Phrase)) {
-                               return false;
-                       }
-                       Phrase phrase = (Phrase) object;
-                       return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
-               }
-
-       }
-
-       /**
-        * A hit consists of a searched object and the score it got for the phrases
-        * of the search.
-        *
-        * @see SearchPage#calculateScore(List, String)
-        * @param <T>
-        *            The type of the searched object
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static class Hit<T> {
-
-               /** Filter for {@link Hit}s with a score of more than 0. */
-               public static final Predicate<Hit<?>> POSITIVE_FILTER = new Predicate<Hit<?>>() {
-
-                       @Override
-                       public boolean apply(Hit<?> hit) {
-                               return (hit != null) && (hit.getScore() > 0);
-                       }
-
-               };
-
-               /** Comparator that sorts {@link Hit}s descending by score. */
-               public static final Comparator<Hit<?>> DESCENDING_COMPARATOR = new Comparator<Hit<?>>() {
-
-                       @Override
-                       public int compare(Hit<?> leftHit, Hit<?> rightHit) {
-                               return Double.compare(rightHit.getScore(), leftHit.getScore());
-                       }
-
-               };
-
-               /** The object that was searched. */
-               private final T object;
-
-               /** The score of the object. */
-               private final double score;
-
-               /**
-                * Creates a new hit.
-                *
-                * @param object
-                *            The object that was searched
-                * @param score
-                *            The score of the object
-                */
-               public Hit(T object, double score) {
-                       this.object = object;
-                       this.score = score;
-               }
-
-               /**
-                * Returns the object that was searched.
-                *
-                * @return The object that was searched
-                */
-               public T getObject() {
-                       return object;
-               }
-
-               /**
-                * Returns the score of the object.
-                *
-                * @return The score of the object
-                */
-               public double getScore() {
-                       return score;
-               }
-
-       }
-
-       /**
-        * Extracts the object from a {@link Hit}.
-        *
-        * @param <T>
-        *            The type of the object to extract
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private static class HitMapper<T> implements Function<Hit<T>, T> {
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public T apply(Hit<T> input) {
-                       return input.getObject();
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
deleted file mode 100644 (file)
index bece45d..0000000
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Sone - SoneTemplatePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nonnull;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.sone.web.page.FreenetTemplatePage;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import freenet.clients.http.SessionManager.Session;
-import freenet.clients.http.ToadletContext;
-import freenet.support.api.HTTPRequest;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
-/**
- * Base page for the Sone web interface.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class SoneTemplatePage extends FreenetTemplatePage {
-
-       /** The Sone core. */
-       protected final WebInterface webInterface;
-
-       /** The page title l10n key. */
-       private final String pageTitleKey;
-
-       /** Whether to require a login. */
-       private final boolean requireLogin;
-
-       /**
-        * Creates a new template page for Sone that does not require the user to be
-        * logged in.
-        *
-        * @param path
-        *            The path of the page
-        * @param template
-        *            The template to render
-        * @param pageTitleKey
-        *            The l10n key of the page title
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface) {
-               this(path, template, pageTitleKey, webInterface, false);
-       }
-
-       /**
-        * Creates a new template page for Sone.
-        *
-        * @param path
-        *            The path of the page
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        * @param requireLogin
-        *            Whether this page requires a login
-        */
-       public SoneTemplatePage(String path, Template template, WebInterface webInterface, boolean requireLogin) {
-               this(path, template, null, webInterface, requireLogin);
-       }
-
-       /**
-        * Creates a new template page for Sone.
-        *
-        * @param path
-        *            The path of the page
-        * @param template
-        *            The template to render
-        * @param pageTitleKey
-        *            The l10n key of the page title
-        * @param webInterface
-        *            The Sone web interface
-        * @param requireLogin
-        *            Whether this page requires a login
-        */
-       public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface, boolean requireLogin) {
-               super(path, webInterface.getTemplateContextFactory(), template, "noPermission.html");
-               this.pageTitleKey = pageTitleKey;
-               this.webInterface = webInterface;
-               this.requireLogin = requireLogin;
-       }
-
-       //
-       // PROTECTED METHODS
-       //
-
-       /**
-        * 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
-        */
-       protected Sone getCurrentSone(ToadletContext toadletContext) {
-               return webInterface.getCurrentSoneCreatingSession(toadletContext);
-       }
-
-       protected Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
-               return webInterface.getCurrentSoneWithoutCreatingSession(toadletContext);
-       }
-
-       /**
-        * Sets the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @param sone
-        *            The Sone to set as currently logged in
-        */
-       protected void setCurrentSone(ToadletContext toadletContext, Sone sone) {
-               webInterface.setCurrentSone(toadletContext, sone);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getPageTitle(FreenetRequest request) {
-               if (pageTitleKey != null) {
-                       return webInterface.getL10n().getString(pageTitleKey);
-               }
-               return "";
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
-               return ImmutableList.<Map<String, String>> builder().add(ImmutableMap.<String, String> builder().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").build()).build();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected Collection<String> getStyleSheets() {
-               return Arrays.asList("css/sone.css");
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getShortcutIcon() {
-               return "images/icon.png";
-       }
-
-       /**
-        * Returns whether this page requires the user to log in.
-        *
-        * @return {@code true} if the user is required to be logged in to use this
-        *         page, {@code false} otherwise
-        */
-       protected boolean requiresLogin() {
-               return requireLogin;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected final void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               super.processTemplate(request, templateContext);
-               Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
-               templateContext.set("preferences", webInterface.getCore().getPreferences());
-               templateContext.set("currentSone", currentSone);
-               templateContext.set("localSones", webInterface.getCore().getLocalSones());
-               templateContext.set("request", request);
-               templateContext.set("currentVersion", SonePlugin.getPluginVersion());
-               templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
-               templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
-               templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
-               templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
-               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
-               Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
-               templateContext.set("notifications", notifications);
-               templateContext.set("notificationHash", notifications.hashCode());
-               handleRequest(request, templateContext);
-       }
-
-       protected void handleRequest(@Nonnull FreenetRequest request, @Nonnull TemplateContext templateContext) throws RedirectException {
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getRedirectTarget(FreenetRequest request) {
-               if (requiresLogin() && (getCurrentSoneWithoutCreatingSession(request.getToadletContext()) == null)) {
-                       HTTPRequest httpRequest = request.getHttpRequest();
-                       String originalUrl = httpRequest.getPath();
-                       if (httpRequest.hasParameters()) {
-                               StringBuilder requestParameters = new StringBuilder();
-                               for (String parameterName : httpRequest.getParameterNames()) {
-                                       if (requestParameters.length() > 0) {
-                                               requestParameters.append("&");
-                                       }
-                                       String[] parameterValues = httpRequest.getMultipleParam(parameterName);
-                                       for (String parameterValue : parameterValues) {
-                                               requestParameters.append(urlEncode(parameterName)).append("=").append(urlEncode(parameterValue));
-                                       }
-                               }
-                               originalUrl += "?" + requestParameters.toString();
-                       }
-                       return "login.html?target=" + urlEncode(originalUrl);
-               }
-               return null;
-       }
-
-       private static String urlEncode(String value) {
-               try {
-                       return URLEncoder.encode(value, "UTF-8");
-               } catch (UnsupportedEncodingException uee1) {
-                                                       /* A JVM without UTF-8? I don’t think so. */
-                       throw new RuntimeException(uee1);
-               }
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean isFullAccessOnly() {
-               return webInterface.getCore().getPreferences().isRequireFullAccess();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isEnabled(ToadletContext toadletContext) {
-               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
-                       return false;
-               }
-               if (requiresLogin()) {
-                       return getCurrentSoneWithoutCreatingSession(toadletContext) != null;
-               }
-               return true;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/TrustPage.java b/src/main/java/net/pterodactylus/sone/web/TrustPage.java
deleted file mode 100644 (file)
index 5b0bfbb..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - TrustPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user trust another Sone. This will assign a configurable
- * amount of trust to an identity.
- *
- * @see Core#trustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TrustPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “trust Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public TrustPage(Template template, WebInterface webInterface) {
-               super("trust.html", template, "Page.Trust.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       Optional<Sone> sone = webInterface.getCore().getSone(identity);
-                       if (sone.isPresent()) {
-                               webInterface.getCore().trustSone(currentSone, sone.get());
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
deleted file mode 100644 (file)
index 9cc266e..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Sone - UnbookmarkPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.util.Set;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-import com.google.common.base.Optional;
-
-/**
- * Page that lets the user unbookmark a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnbookmarkPage extends SoneTemplatePage {
-
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnbookmarkPage(Template template, WebInterface webInterface) {
-               super("unbookmark.html", template, "Page.Unbookmark.Title", webInterface);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Optional<Post> post = webInterface.getCore().getPost(id);
-                       if (post.isPresent()) {
-                               webInterface.getCore().unbookmarkPost(post.get());
-                       }
-                       throw new RedirectException(returnPage);
-               }
-               String id = request.getHttpRequest().getParam("post");
-               if (id.equals("allNotLoaded")) {
-                       Set<Post> posts = webInterface.getCore().getBookmarkedPosts();
-                       for (Post post : posts) {
-                               if (!post.isLoaded()) {
-                                       webInterface.getCore().unbookmarkPost(post);
-                               }
-                       }
-                       throw new RedirectException("bookmarks.html");
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
deleted file mode 100644 (file)
index bff4013..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - UnfollowSonePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * This page lets the user unfollow another Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnfollowSonePage extends SoneTemplatePage {
-
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnfollowSonePage(Template template, WebInterface webInterface) {
-               super("unfollowSone.html", template, "Page.UnfollowSone.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
-                       for (String soneId : soneIds.split("[ ,]+")) {
-                               webInterface.getCore().unfollowSone(currentSone, soneId);
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnlikePage.java b/src/main/java/net/pterodactylus/sone/web/UnlikePage.java
deleted file mode 100644 (file)
index a17502d..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Sone - UnlikePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user unlike a {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnlikePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “unlike post” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnlikePage(Template template, WebInterface webInterface) {
-               super("unlike.html", template, "Page.Unlike.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
-                       String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       if ("post".equals(type)) {
-                               currentSone.removeLikedPostId(id);
-                       } else if ("reply".equals(type)) {
-                               currentSone.removeLikedReplyId(id);
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java b/src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
deleted file mode 100644 (file)
index 7c36a88..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Sone - UnlockSonePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * This page lets the user unlock a {@link Sone} to allow its insertion.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnlockSonePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “unlock Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnlockSonePage(Template template, WebInterface webInterface) {
-               super("unlockSone.html", template, "Page.UnlockSone.Title", webInterface);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
-               Sone sone = webInterface.getCore().getLocalSone(soneId);
-               if (sone != null) {
-                       webInterface.getCore().unlockSone(sone);
-               }
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               throw new RedirectException(returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UntrustPage.java b/src/main/java/net/pterodactylus/sone/web/UntrustPage.java
deleted file mode 100644 (file)
index cba3635..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - UntrustPage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-/**
- * Page that lets the user untrust another Sone. This will remove all trust
- * assignments for an identity.
- *
- * @see Core#untrustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UntrustPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “untrust Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UntrustPage(Template template, WebInterface webInterface) {
-               super("untrust.html", template, "Page.Untrust.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       Optional<Sone> sone = webInterface.getCore().getSone(identity);
-                       if (sone.isPresent()) {
-                               webInterface.getCore().untrustSone(currentSone, sone.get());
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java b/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
deleted file mode 100644 (file)
index 467ceda..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Sone - UploadImagePage.java - Copyright © 2011–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static com.google.common.base.Optional.fromNullable;
-import static java.util.logging.Logger.getLogger;
-
-import java.awt.Image;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Iterator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.imageio.ImageIO;
-import javax.imageio.ImageReader;
-import javax.imageio.stream.ImageInputStream;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Image.Modifier.ImageTitleMustNotBeEmpty;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-
-import com.google.common.io.ByteStreams;
-
-import freenet.support.api.Bucket;
-import freenet.support.api.HTTPUploadedFile;
-
-/**
- * Page implementation that lets the user upload an image.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UploadImagePage extends SoneTemplatePage {
-
-       private static final Logger logger = getLogger(UploadImagePage.class.getName());
-       private static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
-
-       /**
-        * Creates a new “upload image” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UploadImagePage(Template template, WebInterface webInterface) {
-               super("uploadImage.html", template, "Page.UploadImage.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
-                       Album parent = webInterface.getCore().getAlbum(parentId);
-                       if (parent == null) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if (!currentSone.equals(parent.getSone())) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       String name = request.getHttpRequest().getPartAsStringFailsafe("title", 200).trim();
-                       if (name.length() == 0) {
-                               throw new RedirectException("emptyImageTitle.html");
-                       }
-                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 4000);
-                       HTTPUploadedFile uploadedFile = request.getHttpRequest().getUploadedFile("image");
-                       Bucket fileBucket = uploadedFile.getData();
-                       InputStream imageInputStream = null;
-                       ByteArrayOutputStream imageDataOutputStream = null;
-                       try {
-                               imageInputStream = fileBucket.getInputStream();
-                               /* TODO - check length */
-                               imageDataOutputStream = new ByteArrayOutputStream((int) fileBucket.size());
-                               ByteStreams.copy(imageInputStream, imageDataOutputStream);
-                       } catch (IOException ioe1) {
-                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
-                               return;
-                       } finally {
-                               fileBucket.free();
-                               Closer.close(imageInputStream);
-                               Closer.close(imageDataOutputStream);
-                       }
-                       byte[] imageData = imageDataOutputStream.toByteArray();
-                       ByteArrayInputStream imageDataInputStream = null;
-                       Image uploadedImage = null;
-                       try {
-                               imageDataInputStream = new ByteArrayInputStream(imageData);
-                               uploadedImage = ImageIO.read(imageDataInputStream);
-                               if (uploadedImage == null) {
-                                       templateContext.set("messages", webInterface.getL10n().getString("Page.UploadImage.Error.InvalidImage"));
-                                       return;
-                               }
-                               String mimeType = getMimeType(imageData);
-                               TemporaryImage temporaryImage = webInterface.getCore().createTemporaryImage(mimeType, imageData);
-                               net.pterodactylus.sone.data.Image image = webInterface.getCore().createImage(currentSone, parent, temporaryImage);
-                               image.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null)).update();
-                       } catch (IOException ioe1) {
-                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
-                               return;
-                       } catch (ImageTitleMustNotBeEmpty itmnbe) {
-                               throw new RedirectException("emptyImageTitle.html");
-                       } finally {
-                               Closer.close(imageDataInputStream);
-                               Closer.flush(uploadedImage);
-                       }
-                       throw new RedirectException("imageBrowser.html?album=" + parent.getId());
-               }
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Tries to detect the MIME type of the encoded image.
-        *
-        * @param imageData
-        *            The encoded image
-        * @return The MIME type of the image, or “application/octet-stream” if the
-        *         image type could not be detected
-        */
-       private static String getMimeType(byte[] imageData) {
-               ByteArrayInputStream imageDataInputStream = new ByteArrayInputStream(imageData);
-               try {
-                       ImageInputStream imageInputStream = ImageIO.createImageInputStream(imageDataInputStream);
-                       Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
-                       if (imageReaders.hasNext()) {
-                               return fromNullable(imageReaders.next().getOriginatingProvider().getMIMETypes())
-                                               .or(new String[] { UNKNOWN_MIME_TYPE })[0];
-                       }
-               } catch (IOException ioe1) {
-                       logger.log(Level.FINE, "Could not detect MIME type for image.", ioe1);
-               }
-               return UNKNOWN_MIME_TYPE;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java b/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
deleted file mode 100644 (file)
index 56bac8d..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Sone - ViewPostPage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import java.net.URI;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.template.SoneAccessor;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * This page lets the user view a post and all its replies.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ViewPostPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “view post” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public ViewPostPage(Template template, WebInterface webInterface) {
-               super("viewPost.html", template, "Page.ViewPost.Title", webInterface, false);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getPageTitle(FreenetRequest request) {
-               String postId = request.getHttpRequest().getParam("post");
-               Optional<Post> post = webInterface.getCore().getPost(postId);
-               String title = "";
-               if (post.isPresent()) {
-                       title = post.get().getText().substring(0, Math.min(20, post.get().getText().length())) + "…";
-                       title += " - " + SoneAccessor.getNiceName(post.get().getSone()) + " - ";
-               }
-               title += webInterface.getL10n().getString("Page.ViewPost.Title");
-               return title;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String postId = request.getHttpRequest().getParam("post");
-               boolean raw = request.getHttpRequest().getParam("raw").equals("true");
-               Optional<Post> post = webInterface.getCore().getPost(postId);
-               templateContext.set("post", post.orNull());
-               templateContext.set("raw", raw);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return true;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
deleted file mode 100644 (file)
index f2c9c91..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Sone - ViewSonePage.java - Copyright © 2010–2016 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.template.SoneAccessor;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.collection.Pagination;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.base.Optional;
-
-/**
- * Lets the user browser another Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ViewSonePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “view Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public ViewSonePage(Template template, WebInterface webInterface) {
-               super("viewSone.html", template, webInterface, false);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getPageTitle(FreenetRequest request) {
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               if (sone.isPresent()) {
-                       String soneName = SoneAccessor.getNiceName(sone.get());
-                       return soneName + " - " + webInterface.getL10n().getString("Page.ViewSone.Title");
-               }
-               return webInterface.getL10n().getString("Page.ViewSone.Page.TitleWithoutSone");
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               templateContext.set("sone", sone.orNull());
-               templateContext.set("soneId", soneId);
-               if (!sone.isPresent()) {
-                       return;
-               }
-               List<Post> sonePosts = sone.get().getPosts();
-               sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone.get().getId()));
-               Collections.sort(sonePosts, Post.NEWEST_FIRST);
-               Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("postPage"), 0));
-               templateContext.set("postPagination", postPagination);
-               templateContext.set("posts", postPagination.getItems());
-               Set<PostReply> replies = sone.get().getReplies();
-               final Map<Post, List<PostReply>> repliedPosts = new HashMap<Post, List<PostReply>>();
-               for (PostReply reply : replies) {
-                       Optional<Post> post = reply.getPost();
-                       if (!post.isPresent() || repliedPosts.containsKey(post.get()) || sone.get().equals(post.get().getSone()) || (sone.get().getId().equals(post.get().getRecipientId().orNull()))) {
-                               continue;
-                       }
-                       repliedPosts.put(post.get(), webInterface.getCore().getReplies(post.get().getId()));
-               }
-               List<Post> posts = new ArrayList<Post>(repliedPosts.keySet());
-               Collections.sort(posts, new Comparator<Post>() {
-
-                       @Override
-                       public int compare(Post leftPost, Post rightPost) {
-                               return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, repliedPosts.get(rightPost).get(0).getTime() - repliedPosts.get(leftPost).get(0).getTime()));
-                       }
-
-               });
-
-               Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("repliedPostPage"), 0));
-               templateContext.set("repliedPostPagination", repliedPostPagination);
-               templateContext.set("repliedPosts", repliedPostPagination.getItems());
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return true;
-       }
-
-}
index 6ce38d4..ee309df 100644 (file)
@@ -140,6 +140,49 @@ 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.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.PageToadlet;
 import net.pterodactylus.sone.web.page.PageToadletFactory;
+import net.pterodactylus.sone.web.pages.AboutPage;
+import net.pterodactylus.sone.web.pages.BookmarkPage;
+import net.pterodactylus.sone.web.pages.BookmarksPage;
+import net.pterodactylus.sone.web.pages.CreateAlbumPage;
+import net.pterodactylus.sone.web.pages.CreatePostPage;
+import net.pterodactylus.sone.web.pages.CreateReplyPage;
+import net.pterodactylus.sone.web.pages.CreateSonePage;
+import net.pterodactylus.sone.web.pages.DeleteAlbumPage;
+import net.pterodactylus.sone.web.pages.DeleteImagePage;
+import net.pterodactylus.sone.web.pages.DeletePostPage;
+import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage;
+import net.pterodactylus.sone.web.pages.DeleteReplyPage;
+import net.pterodactylus.sone.web.pages.DeleteSonePage;
+import net.pterodactylus.sone.web.pages.DismissNotificationPage;
+import net.pterodactylus.sone.web.pages.DistrustPage;
+import net.pterodactylus.sone.web.pages.EditAlbumPage;
+import net.pterodactylus.sone.web.pages.EditImagePage;
+import net.pterodactylus.sone.web.pages.EditProfileFieldPage;
+import net.pterodactylus.sone.web.pages.EditProfilePage;
+import net.pterodactylus.sone.web.pages.FollowSonePage;
+import net.pterodactylus.sone.web.pages.GetImagePage;
+import net.pterodactylus.sone.web.pages.ImageBrowserPage;
+import net.pterodactylus.sone.web.pages.IndexPage;
+import net.pterodactylus.sone.web.pages.KnownSonesPage;
+import net.pterodactylus.sone.web.pages.LikePage;
+import net.pterodactylus.sone.web.pages.LockSonePage;
+import net.pterodactylus.sone.web.pages.LoginPage;
+import net.pterodactylus.sone.web.pages.LogoutPage;
+import net.pterodactylus.sone.web.pages.MarkAsKnownPage;
+import net.pterodactylus.sone.web.pages.NewPage;
+import net.pterodactylus.sone.web.pages.OptionsPage;
+import net.pterodactylus.sone.web.pages.RescuePage;
+import net.pterodactylus.sone.web.pages.SearchPage;
+import net.pterodactylus.sone.web.pages.SoneTemplatePage;
+import net.pterodactylus.sone.web.pages.TrustPage;
+import net.pterodactylus.sone.web.pages.UnbookmarkPage;
+import net.pterodactylus.sone.web.pages.UnfollowSonePage;
+import net.pterodactylus.sone.web.pages.UnlikePage;
+import net.pterodactylus.sone.web.pages.UnlockSonePage;
+import net.pterodactylus.sone.web.pages.UntrustPage;
+import net.pterodactylus.sone.web.pages.UploadImagePage;
+import net.pterodactylus.sone.web.pages.ViewPostPage;
+import net.pterodactylus.sone.web.pages.ViewSonePage;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/EditImagePage.java b/src/main/java/net/pterodactylus/sone/web/pages/EditImagePage.java
new file mode 100644 (file)
index 0000000..34f404e
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Sone - EditImagePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit title and description of an {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditImagePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditImagePage(Template template, WebInterface webInterface) {
+               super("editImage.html", template, "Page.EditImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       if (image == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!image.getSone().isLocal()) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
+                               image.getAlbum().moveImageUp(image);
+                       } else if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
+                               image.getAlbum().moveImageDown(image);
+                       } else {
+                               String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
+                               String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1024).trim();
+                               if (title.length() == 0) {
+                                       throw new RedirectException("emptyImageTitle.html");
+                               }
+                               image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
+                       }
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/EditProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/pages/EditProfileFieldPage.java
new file mode 100644 (file)
index 0000000..2bfdddf
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Sone - EditProfileFieldPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit the name of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldPage(Template template, WebInterface webInterface) {
+               super("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process the POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("cancel", 4).equals("true")) {
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       fieldId = request.getHttpRequest().getPartAsStringFailsafe("field", 36);
+                       field = profile.getFieldById(fieldId);
+                       if (field == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 256);
+                       Field existingField = profile.getFieldByName(name);
+                       if ((existingField != null) && (!field.equals(existingField))) {
+                               templateContext.set("duplicateFieldName", true);
+                       } else {
+                               if (!name.equals(field.getName())) {
+                                       field.setName(name);
+                                       currentSone.setProfile(profile);
+                               }
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+               }
+
+               /* store current values in template. */
+               templateContext.set("field", field);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/EditProfilePage.java b/src/main/java/net/pterodactylus/sone/web/pages/EditProfilePage.java
new file mode 100644 (file)
index 0000000..b17a22e
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Sone - EditProfilePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.text.TextFilter.filter;
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.util.List;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.DuplicateField;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+import freenet.clients.http.ToadletContext;
+
+/**
+ * This page lets the user edit her profile.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfilePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit profile” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfilePage(Template template, WebInterface webInterface) {
+               super("editProfile.html", template, "Page.EditProfile.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               ToadletContext toadletContenxt = request.getToadletContext();
+               Sone currentSone = getCurrentSone(toadletContenxt);
+               Profile profile = currentSone.getProfile();
+               String firstName = profile.getFirstName();
+               String middleName = profile.getMiddleName();
+               String lastName = profile.getLastName();
+               Integer birthDay = profile.getBirthDay();
+               Integer birthMonth = profile.getBirthMonth();
+               Integer birthYear = profile.getBirthYear();
+               String avatarId = profile.getAvatar();
+               List<Field> fields = profile.getFields();
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("save-profile", 4).equals("true")) {
+                               firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
+                               middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
+                               lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
+                               birthDay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim(), null);
+                               birthMonth = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim(), null);
+                               birthYear = parseInt(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim(), null);
+                               avatarId = request.getHttpRequest().getPartAsStringFailsafe("avatarId", 36);
+                               profile.setFirstName(firstName.length() > 0 ? firstName : null);
+                               profile.setMiddleName(middleName.length() > 0 ? middleName : null);
+                               profile.setLastName(lastName.length() > 0 ? lastName : null);
+                               profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
+                               profile.setAvatar(webInterface.getCore().getImage(avatarId, false));
+                               for (Field field : fields) {
+                                       String value = request.getHttpRequest().getPartAsStringFailsafe("field-" + field.getId(), 400);
+                                       String filteredValue = filter(request.getHttpRequest().getHeader("Host"), value);
+                                       field.setValue(filteredValue);
+                               }
+                               currentSone.setProfile(profile);
+                               webInterface.getCore().touchConfiguration();
+                               throw new RedirectException("editProfile.html");
+                       } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
+                               String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
+                               try {
+                                       profile.addField(fieldName);
+                                       currentSone.setProfile(profile);
+                                       webInterface.getCore().touchConfiguration();
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               } catch (DuplicateField df1) {
+                                       templateContext.set("fieldName", fieldName);
+                                       templateContext.set("duplicateFieldName", true);
+                               }
+                       } else {
+                               String id = getFieldId(request, "delete-field-");
+                               if (id != null) {
+                                       throw new RedirectException("deleteProfileField.html?field=" + id);
+                               }
+                               id = getFieldId(request, "move-up-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldUp(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "move-down-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldDown(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "edit-field-");
+                               if (id != null) {
+                                       throw new RedirectException("editProfileField.html?field=" + id);
+                               }
+                       }
+               }
+               templateContext.set("firstName", firstName);
+               templateContext.set("middleName", middleName);
+               templateContext.set("lastName", lastName);
+               templateContext.set("birthDay", birthDay);
+               templateContext.set("birthMonth", birthMonth);
+               templateContext.set("birthYear", birthYear);
+               templateContext.set("avatarId", avatarId);
+               templateContext.set("fields", fields);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Searches for a part whose names starts with the given {@code String} and
+        * extracts the ID from the located name.
+        *
+        * @param request
+        *            The request to get the parts from
+        * @param partNameStart
+        *            The start of the name of the requested part
+        * @return The parsed ID, or {@code null} if there was no part matching the
+        *         given string
+        */
+       private static String getFieldId(FreenetRequest request, String partNameStart) {
+               for (String partName : request.getHttpRequest().getParts()) {
+                       if (partName.startsWith(partNameStart)) {
+                               return partName.substring(partNameStart.length());
+                       }
+               }
+               return null;
+       }
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/FollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/pages/FollowSonePage.java
new file mode 100644 (file)
index 0000000..9bfb790
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Sone - FollowSonePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import com.google.common.base.Optional;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * This page lets the user follow another Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FollowSonePage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public FollowSonePage(Template template, WebInterface webInterface) {
+               super("followSone.html", template, "Page.FollowSone.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
+                               if (sone.isPresent()) {
+                                       webInterface.getCore().followSone(currentSone, soneId);
+                                       webInterface.getCore().markSoneKnown(sone.get());
+                               }
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/GetImagePage.java b/src/main/java/net/pterodactylus/sone/web/pages/GetImagePage.java
new file mode 100644 (file)
index 0000000..bcf0eda
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Sone - GetImagePage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.io.IOException;
+import java.net.URI;
+
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetPage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.web.Response;
+
+/**
+ * Page that delivers a {@link TemporaryImage} to the browser.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetImagePage implements FreenetPage {
+
+       /** The Sone web interface. */
+       private final WebInterface webInterface;
+
+       /**
+        * Creates a new “get image” page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetImagePage(WebInterface webInterface) {
+               this.webInterface = webInterface;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return "getImage.html";
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+               String imageId = request.getHttpRequest().getParam("image");
+               TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
+               if (temporaryImage == null) {
+                       return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
+               }
+               String contentType= temporaryImage.getMimeType();
+               return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return false;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/ImageBrowserPage.java b/src/main/java/net/pterodactylus/sone/web/pages/ImageBrowserPage.java
new file mode 100644 (file)
index 0000000..2e53761
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Sone - ImageBrowserPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static com.google.common.collect.FluentIterable.from;
+import static net.pterodactylus.sone.data.Album.FLATTENER;
+import static net.pterodactylus.sone.data.Album.NOT_EMPTY;
+import static net.pterodactylus.sone.data.Album.TITLE_COMPARATOR;
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.common.base.Optional;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The image browser page is the entry page for the image management.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageBrowserPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new image browser page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public ImageBrowserPage(Template template, WebInterface webInterface) {
+               super("imageBrowser.html", template, "Page.ImageBrowser.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String albumId = request.getHttpRequest().getParam("album", null);
+               if (albumId != null) {
+                       Album album = webInterface.getCore().getAlbum(albumId);
+                       templateContext.set("albumRequested", true);
+                       templateContext.set("album", album);
+                       templateContext.set("page", request.getHttpRequest().getParam("page"));
+                       return;
+               }
+               String imageId = request.getHttpRequest().getParam("image", null);
+               if (imageId != null) {
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       templateContext.set("imageRequested", true);
+                       templateContext.set("image", image);
+                       return;
+               }
+               String soneId = request.getHttpRequest().getParam("sone", null);
+               if (soneId != null) {
+                       Optional<Sone> sone = webInterface.getCore().getSone(soneId);
+                       templateContext.set("soneRequested", true);
+                       templateContext.set("sone", sone.orNull());
+                       return;
+               }
+               String mode = request.getHttpRequest().getParam("mode", null);
+               if ("gallery".equals(mode)) {
+                       templateContext.set("galleryRequested", true);
+                       List<Album> albums = new ArrayList<Album>();
+                       for (Sone sone : webInterface.getCore().getSones()) {
+                               albums.addAll(from(sone.getRootAlbum().getAlbums()).transformAndConcat(FLATTENER).filter(NOT_EMPTY).toList());
+                       }
+                       Collections.sort(albums, TITLE_COMPARATOR);
+                       Pagination<Album> albumPagination = new Pagination<Album>(albums, 12).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
+                       templateContext.set("albumPagination", albumPagination);
+                       templateContext.set("albums", albumPagination.getItems());
+                       return;
+               }
+               Sone sone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
+               templateContext.set("soneRequested", true);
+               templateContext.set("sone", sone);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/IndexPage.java b/src/main/java/net/pterodactylus/sone/web/pages/IndexPage.java
new file mode 100644 (file)
index 0000000..5946a57
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sone - IndexPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Collections2;
+
+/**
+ * The index page shows the main page of Sone. This page will contain the posts
+ * of all friends of the current user.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class IndexPage extends SoneTemplatePage {
+
+       private final PostVisibilityFilter postVisibilityFilter;
+
+       public IndexPage(Template template, WebInterface webInterface, PostVisibilityFilter postVisibilityFilter) {
+               super("index.html", template, "Page.Index.Title", webInterface, true);
+               this.postVisibilityFilter = postVisibilityFilter;
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               final Sone currentSone = getCurrentSone(request.getToadletContext());
+               Collection<Post> allPosts = new ArrayList<Post>();
+               allPosts.addAll(currentSone.getPosts());
+               for (String friendSoneId : currentSone.getFriends()) {
+                       Optional<Sone> friendSone = webInterface.getCore().getSone(friendSoneId);
+                       if (!friendSone.isPresent()) {
+                               continue;
+                       }
+                       allPosts.addAll(friendSone.get().getPosts());
+               }
+               for (Sone sone : webInterface.getCore().getSones()) {
+                       for (Post post : sone.getPosts()) {
+                               if (currentSone.equals(post.getRecipient().orNull()) && !allPosts.contains(post)) {
+                                       allPosts.add(post);
+                               }
+                       }
+               }
+               allPosts = Collections2.filter(allPosts, postVisibilityFilter.isVisible(currentSone));
+               List<Post> sortedPosts = new ArrayList<Post>(allPosts);
+               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
+               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", pagination);
+               templateContext.set("posts", pagination.getItems());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/KnownSonesPage.java b/src/main/java/net/pterodactylus/sone/web/pages/KnownSonesPage.java
new file mode 100644 (file)
index 0000000..5aef1f9
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Sone - KnownSonesPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Ordering;
+
+/**
+ * This page shows all known Sones.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class KnownSonesPage extends SoneTemplatePage {
+
+       private static final String defaultSortField = "activity";
+       private static final String defaultSortOrder = "desc";
+
+       /**
+        * Creates a “known Sones” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public KnownSonesPage(Template template, WebInterface webInterface) {
+               super("knownSones.html", template, "Page.KnownSones.Title", webInterface, false);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String sortField = request.getHttpRequest().getParam("sort", defaultSortField);
+               String sortOrder = request.getHttpRequest().getParam("order", defaultSortOrder);
+               String filter = request.getHttpRequest().getParam("filter");
+               templateContext.set("sort", sortField);
+               templateContext.set("order", sortOrder);
+               templateContext.set("filter", filter);
+               final Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
+               Collection<Sone> knownSones = Collections2.filter(webInterface.getCore().getSones(), Sone.EMPTY_SONE_FILTER);
+               if ((currentSone != null) && "followed".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
+
+                               @Override
+                               public boolean apply(Sone sone) {
+                                       return currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               } else if ((currentSone != null) && "not-followed".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
+
+                               @Override
+                               public boolean apply(Sone sone) {
+                                       return !currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               } else if ("new".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
+
+                               /**
+                                * {@inheritDoc}
+                                */
+                               @Override
+                               public boolean apply(Sone sone) {
+                                       return !sone.isKnown();
+                               }
+                       });
+               } else if ("not-new".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, new Predicate<Sone>() {
+
+                               /**
+                                * {@inheritDoc}
+                                */
+                               @Override
+                               public boolean apply(Sone sone) {
+                                       return sone.isKnown();
+                               }
+                       });
+               } else if ("own".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, Sone.LOCAL_SONE_FILTER);
+               } else if ("not-own".equals(filter)) {
+                       knownSones = Collections2.filter(knownSones, Predicates.not(Sone.LOCAL_SONE_FILTER));
+               }
+               List<Sone> sortedSones = new ArrayList<Sone>(knownSones);
+               if ("activity".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(sortedSones, Ordering.from(Sone.LAST_ACTIVITY_COMPARATOR).reverse());
+                       } else {
+                               Collections.sort(sortedSones, Sone.LAST_ACTIVITY_COMPARATOR);
+                       }
+               } else if ("posts".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(sortedSones, Ordering.from(Sone.POST_COUNT_COMPARATOR).reverse());
+                       } else {
+                               Collections.sort(sortedSones, Sone.POST_COUNT_COMPARATOR);
+                       }
+               } else if ("images".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(sortedSones, Ordering.from(Sone.IMAGE_COUNT_COMPARATOR).reverse());
+                       } else {
+                               Collections.sort(sortedSones, Sone.IMAGE_COUNT_COMPARATOR);
+                       }
+               } else {
+                       if ("desc".equals(sortOrder)) {
+                               Collections.sort(sortedSones, Ordering.from(Sone.NICE_NAME_COMPARATOR).reverse());
+                       } else {
+                               Collections.sort(sortedSones, Sone.NICE_NAME_COMPARATOR);
+                       }
+               }
+               Pagination<Sone> sonePagination = new Pagination<Sone>(sortedSones, 25).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", sonePagination);
+               templateContext.set("knownSones", sonePagination.getItems());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/LikePage.java b/src/main/java/net/pterodactylus/sone/web/pages/LikePage.java
new file mode 100644 (file)
index 0000000..529b620
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Sone - LikePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user like a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “like post” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public LikePage(Template template, WebInterface webInterface) {
+               super("like.html", template, "Page.Like.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+                       String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       if ("post".equals(type)) {
+                               currentSone.addLikedPostId(id);
+                       } else if ("reply".equals(type)) {
+                               currentSone.addLikedReplyId(id);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/LockSonePage.java b/src/main/java/net/pterodactylus/sone/web/pages/LockSonePage.java
new file mode 100644 (file)
index 0000000..8300711
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sone - LockSonePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * This page lets the user lock a {@link Sone} to prevent it from being
+ * inserted.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LockSonePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “lock Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public LockSonePage(Template template, WebInterface webInterface) {
+               super("lockSone.html", template, "Page.LockSone.Title", webInterface);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+               Sone sone = webInterface.getCore().getLocalSone(soneId);
+               if (sone != null) {
+                       webInterface.getCore().lockSone(sone);
+               }
+               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+               throw new RedirectException(returnPage);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/LogoutPage.java b/src/main/java/net/pterodactylus/sone/web/pages/LogoutPage.java
new file mode 100644 (file)
index 0000000..1249086
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Sone - LogoutPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import freenet.clients.http.ToadletContext;
+
+/**
+ * Logs a user out.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LogoutPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public LogoutPage(Template template, WebInterface webInterface) {
+               super("logout.html", template, "Page.Logout.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               setCurrentSone(request.getToadletContext(), null);
+               throw new RedirectException("index.html");
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
+               return (getCurrentSoneWithoutCreatingSession(toadletContext) != null) && (webInterface.getCore().getLocalSones().size() != 1);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/MarkAsKnownPage.java b/src/main/java/net/pterodactylus/sone/web/pages/MarkAsKnownPage.java
new file mode 100644 (file)
index 0000000..4eb7a95
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Sone - MarkAsKnownPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.util.StringTokenizer;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+import com.google.common.base.Optional;
+
+/**
+ * Page that lets the user mark a number of {@link Sone}s, {@link Post}s, or
+ * {@link Reply Replie}s as known.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MarkAsKnownPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “mark as known” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MarkAsKnownPage(Template template, WebInterface webInterface) {
+               super("markAsKnown.html", template, "Page.MarkAsKnown.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
+               if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
+                       throw new RedirectException("invalid.html");
+               }
+               String ids = request.getHttpRequest().getPartAsStringFailsafe("id", 65536);
+               for (StringTokenizer idTokenizer = new StringTokenizer(ids); idTokenizer.hasMoreTokens();) {
+                       String id = idTokenizer.nextToken();
+                       if (type.equals("post")) {
+                               Optional<Post> post = webInterface.getCore().getPost(id);
+                               if (!post.isPresent()) {
+                                       continue;
+                               }
+                               webInterface.getCore().markPostKnown(post.get());
+                       } else if (type.equals("reply")) {
+                               Optional<PostReply> reply = webInterface.getCore().getPostReply(id);
+                               if (!reply.isPresent()) {
+                                       continue;
+                               }
+                               webInterface.getCore().markReplyKnown(reply.get());
+                       } else {
+                               Optional<Sone> sone = webInterface.getCore().getSone(id);
+                               if (!sone.isPresent()) {
+                                       continue;
+                               }
+                               webInterface.getCore().markSoneKnown(sone.get());
+                       }
+               }
+               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+               throw new RedirectException(returnPage);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/NewPage.java b/src/main/java/net/pterodactylus/sone/web/pages/NewPage.java
new file mode 100644 (file)
index 0000000..411f40f
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Sone - NewPage.java - Copyright © 2013–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that displays all new posts and replies. The posts are filtered using
+ * {@link PostVisibilityFilter#isPostVisible(Sone, Post)} and sorted by time.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “new posts and replies” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public NewPage(Template template, WebInterface webInterface) {
+               super("new.html", template, "Page.New.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               /* collect new elements from notifications. */
+               Set<Post> posts = new HashSet<Post>(webInterface.getNewPosts(getCurrentSoneWithoutCreatingSession(request.getToadletContext())));
+               for (PostReply reply : webInterface.getNewReplies(getCurrentSoneWithoutCreatingSession(request.getToadletContext()))) {
+                       posts.add(reply.getPost().get());
+               }
+
+               /* filter and sort them. */
+               List<Post> sortedPosts = new ArrayList<>(posts);
+               Collections.sort(sortedPosts, Post.NEWEST_FIRST);
+
+               /* paginate them. */
+               Pagination<Post> pagination = new Pagination<>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", pagination);
+               templateContext.set("posts", pagination.getItems());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/pages/OptionsPage.java
new file mode 100644 (file)
index 0000000..7b65c33
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * Sone - OptionsPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Preferences;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * This page lets the user edit the options of the Sone plugin.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class OptionsPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new options page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public OptionsPage(Template template, WebInterface webInterface) {
+               super("options.html", template, "Page.Options.Title", webInterface, false);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               Preferences preferences = webInterface.getCore().getPreferences();
+               Sone currentSone = webInterface.getCurrentSoneWithoutCreatingSession(request.getToadletContext());
+               if (request.getMethod() == Method.POST) {
+                       List<String> fieldErrors = new ArrayList<String>();
+                       if (currentSone != null) {
+                               boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
+                               currentSone.getOptions().setAutoFollow(autoFollow);
+                               boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
+                               currentSone.getOptions().setSoneInsertNotificationEnabled(enableSoneInsertNotifications);
+                               boolean showNotificationNewSones = request.getHttpRequest().isPartSet("show-notification-new-sones");
+                               currentSone.getOptions().setShowNewSoneNotifications(showNotificationNewSones);
+                               boolean showNotificationNewPosts = request.getHttpRequest().isPartSet("show-notification-new-posts");
+                               currentSone.getOptions().setShowNewPostNotifications(showNotificationNewPosts);
+                               boolean showNotificationNewReplies = request.getHttpRequest().isPartSet("show-notification-new-replies");
+                               currentSone.getOptions().setShowNewReplyNotifications(showNotificationNewReplies);
+                               String showCustomAvatars = request.getHttpRequest().getPartAsStringFailsafe("show-custom-avatars", 32);
+                               currentSone.getOptions().setShowCustomAvatars(LoadExternalContent.valueOf(showCustomAvatars));
+                               String loadLinkedImages = request.getHttpRequest().getPartAsStringFailsafe("load-linked-images", 32);
+                               currentSone.getOptions().setLoadLinkedImages(LoadExternalContent.valueOf(loadLinkedImages));
+                               webInterface.getCore().touchConfiguration();
+                       }
+                       Integer insertionDelay = parseInt(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16), null);
+                       if (!preferences.validateInsertionDelay(insertionDelay)) {
+                               fieldErrors.add("insertion-delay");
+                       } else {
+                               preferences.setInsertionDelay(insertionDelay);
+                       }
+                       Integer postsPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
+                       if (!preferences.validatePostsPerPage(postsPerPage)) {
+                               fieldErrors.add("posts-per-page");
+                       } else {
+                               preferences.setPostsPerPage(postsPerPage);
+                       }
+                       Integer imagesPerPage = parseInt(request.getHttpRequest().getPartAsStringFailsafe("images-per-page", 4), null);
+                       if (!preferences.validateImagesPerPage(imagesPerPage)) {
+                               fieldErrors.add("images-per-page");
+                       } else {
+                               preferences.setImagesPerPage(imagesPerPage);
+                       }
+                       Integer charactersPerPost = parseInt(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null);
+                       if (!preferences.validateCharactersPerPost(charactersPerPost)) {
+                               fieldErrors.add("characters-per-post");
+                       } else {
+                               preferences.setCharactersPerPost(charactersPerPost);
+                       }
+                       Integer postCutOffLength = parseInt(request.getHttpRequest().getPartAsStringFailsafe("post-cut-off-length", 10), null);
+                       if (!preferences.validatePostCutOffLength(postCutOffLength)) {
+                               fieldErrors.add("post-cut-off-length");
+                       } else {
+                               preferences.setPostCutOffLength(postCutOffLength);
+                       }
+                       boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
+                       preferences.setRequireFullAccess(requireFullAccess);
+                       Integer positiveTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3), null);
+                       if (!preferences.validatePositiveTrust(positiveTrust)) {
+                               fieldErrors.add("positive-trust");
+                       } else {
+                               preferences.setPositiveTrust(positiveTrust);
+                       }
+                       Integer negativeTrust = parseInt(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4), null);
+                       if (!preferences.validateNegativeTrust(negativeTrust)) {
+                               fieldErrors.add("negative-trust");
+                       } else {
+                               preferences.setNegativeTrust(negativeTrust);
+                       }
+                       String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
+                       if (trustComment.trim().length() == 0) {
+                               trustComment = null;
+                       }
+                       preferences.setTrustComment(trustComment);
+                       boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active");
+                       preferences.setFcpInterfaceActive(fcpInterfaceActive);
+                       Integer fcpFullAccessRequiredInteger = parseInt(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
+                       FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
+                       preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
+                       webInterface.getCore().touchConfiguration();
+                       if (fieldErrors.isEmpty()) {
+                               throw new RedirectException(getPath());
+                       }
+                       templateContext.set("fieldErrors", fieldErrors);
+               }
+               if (currentSone != null) {
+                       templateContext.set("auto-follow", currentSone.getOptions().isAutoFollow());
+                       templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().isSoneInsertNotificationEnabled());
+                       templateContext.set("show-notification-new-sones", currentSone.getOptions().isShowNewSoneNotifications());
+                       templateContext.set("show-notification-new-posts", currentSone.getOptions().isShowNewPostNotifications());
+                       templateContext.set("show-notification-new-replies", currentSone.getOptions().isShowNewReplyNotifications());
+                       templateContext.set("show-custom-avatars", currentSone.getOptions().getShowCustomAvatars().name());
+                       templateContext.set("load-linked-images", currentSone.getOptions().getLoadLinkedImages().name());
+               }
+               templateContext.set("insertion-delay", preferences.getInsertionDelay());
+               templateContext.set("posts-per-page", preferences.getPostsPerPage());
+               templateContext.set("images-per-page", preferences.getImagesPerPage());
+               templateContext.set("characters-per-post", preferences.getCharactersPerPost());
+               templateContext.set("post-cut-off-length", preferences.getPostCutOffLength());
+               templateContext.set("require-full-access", preferences.isRequireFullAccess());
+               templateContext.set("positive-trust", preferences.getPositiveTrust());
+               templateContext.set("negative-trust", preferences.getNegativeTrust());
+               templateContext.set("trust-comment", preferences.getTrustComment());
+               templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
+               templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/ReloadingPage.java b/src/main/java/net/pterodactylus/sone/web/pages/ReloadingPage.java
new file mode 100644 (file)
index 0000000..a36c3fd
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Sone - ReloadingPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import net.pterodactylus.util.io.StreamCopier;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Request;
+import net.pterodactylus.util.web.Response;
+
+/**
+ * {@link Page} implementation that delivers static files from the filesystem.
+ *
+ * @param <REQ>
+ *            The type of the request
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReloadingPage<REQ extends Request> implements Page<REQ> {
+
+       private final String pathPrefix;
+       private final String filesystemPath;
+       private final String mimeType;
+
+       public ReloadingPage(String pathPrefix, String filesystemPathPrefix, String mimeType) {
+               this.pathPrefix = pathPrefix;
+               this.filesystemPath = filesystemPathPrefix;
+               this.mimeType = mimeType;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return pathPrefix;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isPrefixPage() {
+               return true;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(REQ request, Response response) throws IOException {
+               String path = request.getUri().getPath();
+               int lastSlash = path.lastIndexOf('/');
+               String filename = path.substring(lastSlash + 1);
+               try (InputStream fileInputStream = new FileInputStream(new File(filesystemPath, filename));
+                       OutputStream contentOutputStream = response.getContent()) {
+                       StreamCopier.copy(fileInputStream, contentOutputStream);
+               } catch (FileNotFoundException fnfe1) {
+                       return response.setStatusCode(404).setStatusText("Not found.");
+               }
+               return response.setStatusCode(200).setStatusText("OK").setContentType(mimeType);
+       }
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/RescuePage.java b/src/main/java/net/pterodactylus/sone/web/pages/RescuePage.java
new file mode 100644 (file)
index 0000000..93ef9e1
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Sone - RescuePage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseLong;
+
+import net.pterodactylus.sone.core.SoneRescuer;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user control the rescue mode for a Sone.
+ *
+ * @see SoneRescuer
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RescuePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new rescue page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public RescuePage(Template template, WebInterface webInterface) {
+               super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
+               SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
+               if (request.getMethod() == Method.POST) {
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
+                               long edition = parseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
+                               if (edition > -1) {
+                                       soneRescuer.setEdition(edition);
+                               }
+                               soneRescuer.startNextFetch();
+                       }
+                       throw new RedirectException("rescue.html");
+               }
+               templateContext.set("soneRescuer", soneRescuer);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/pages/SearchPage.java
new file mode 100644 (file)
index 0000000..ed6c507
--- /dev/null
@@ -0,0 +1,659 @@
+/*
+ * Sone - SearchPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static com.google.common.base.Optional.fromNullable;
+import static com.google.common.primitives.Ints.tryParse;
+import static java.util.logging.Logger.getLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.text.StringEscaper;
+import net.pterodactylus.util.text.TextException;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Ordering;
+
+/**
+ * This page lets the user search for posts and replies that contain certain
+ * words.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SearchPage extends SoneTemplatePage {
+
+       /** The logger. */
+       private static final Logger logger = getLogger(SearchPage.class.getName());
+
+       /** Short-term cache. */
+       private final LoadingCache<List<Phrase>, Set<Hit<Post>>> hitCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<List<Phrase>, Set<Hit<Post>>>() {
+
+               @Override
+               @SuppressWarnings("synthetic-access")
+               public Set<Hit<Post>> load(List<Phrase> phrases) {
+                       Set<Post> posts = new HashSet<Post>();
+                       for (Sone sone : webInterface.getCore().getSones()) {
+                               posts.addAll(sone.getPosts());
+                       }
+                       return getHits(Collections2.filter(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
+               }
+       });
+
+       /**
+        * Creates a new search page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public SearchPage(Template template, WebInterface webInterface) {
+               super("search.html", template, "Page.Search.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       @SuppressWarnings("synthetic-access")
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String query = request.getHttpRequest().getParam("query").trim();
+               if (query.length() == 0) {
+                       throw new RedirectException("index.html");
+               }
+
+               List<Phrase> phrases = parseSearchPhrases(query);
+               if (phrases.isEmpty()) {
+                       throw new RedirectException("index.html");
+               }
+
+               /* check for a couple of shortcuts. */
+               if (phrases.size() == 1) {
+                       String phrase = phrases.get(0).getPhrase();
+
+                       /* is it a Sone ID? */
+                       redirectIfNotNull(getSoneId(phrase), "viewSone.html?sone=");
+
+                       /* is it a post ID? */
+                       redirectIfNotNull(getPostId(phrase), "viewPost.html?post=");
+
+                       /* is it a reply ID? show the post. */
+                       redirectIfNotNull(getReplyPostId(phrase), "viewPost.html?post=");
+
+                       /* is it an album ID? */
+                       redirectIfNotNull(getAlbumId(phrase), "imageBrowser.html?album=");
+
+                       /* is it an image ID? */
+                       redirectIfNotNull(getImageId(phrase), "imageBrowser.html?image=");
+               }
+
+               Collection<Sone> sones = webInterface.getCore().getSones();
+               Collection<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
+
+               Collection<Hit<Post>> postHits = hitCache.getUnchecked(phrases);
+
+               /* now filter. */
+               soneHits = Collections2.filter(soneHits, Hit.POSITIVE_FILTER);
+               postHits = Collections2.filter(postHits, Hit.POSITIVE_FILTER);
+
+               /* now sort. */
+               List<Hit<Sone>> sortedSoneHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(soneHits);
+               List<Hit<Post>> sortedPostHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(postHits);
+
+               /* extract Sones and posts. */
+               List<Sone> resultSones = FluentIterable.from(sortedSoneHits).transform(new HitMapper<Sone>()).toList();
+               List<Post> resultPosts = FluentIterable.from(sortedPostHits).transform(new HitMapper<Post>()).toList();
+
+               /* pagination. */
+               Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("sonePage"))).or(0));
+               Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("postPage"))).or(0));
+
+               templateContext.set("sonePagination", sonePagination);
+               templateContext.set("soneHits", sonePagination.getItems());
+               templateContext.set("postPagination", postPagination);
+               templateContext.set("postHits", postPagination.getItems());
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Collects hit information for the given objects. The objects are converted
+        * to a {@link String} using the given {@link StringGenerator}, and the
+        * {@link #calculateScore(List, String) calculated score} is stored together
+        * with the object in a {@link Hit}, and all resulting {@link Hit}s are then
+        * returned.
+        *
+        * @param <T>
+        *            The type of the objects
+        * @param objects
+        *            The objects to search over
+        * @param phrases
+        *            The phrases to search for
+        * @param stringGenerator
+        *            The string generator for the objects
+        * @return The hits for the given phrases
+        */
+       private static <T> Set<Hit<T>> getHits(Collection<T> objects, List<Phrase> phrases, StringGenerator<T> stringGenerator) {
+               Set<Hit<T>> hits = new HashSet<Hit<T>>();
+               for (T object : objects) {
+                       String objectString = stringGenerator.generateString(object);
+                       double score = calculateScore(phrases, objectString);
+                       hits.add(new Hit<T>(object, score));
+               }
+               return hits;
+       }
+
+       /**
+        * Parses the given query into search phrases. The query is split on
+        * whitespace while allowing to group words using single or double quotes.
+        * Isolated phrases starting with a “+” are
+        * {@link Phrase.Optionality#REQUIRED}, phrases with a “-” are
+        * {@link Phrase.Optionality#FORBIDDEN}.
+        *
+        * @param query
+        *            The query to parse
+        * @return The parsed phrases
+        */
+       private static List<Phrase> parseSearchPhrases(String query) {
+               List<String> parsedPhrases;
+               try {
+                       parsedPhrases = StringEscaper.parseLine(query);
+               } catch (TextException te1) {
+                       /* invalid query. */
+                       return Collections.emptyList();
+               }
+
+               List<Phrase> phrases = new ArrayList<Phrase>();
+               for (String phrase : parsedPhrases) {
+                       if (phrase.startsWith("+")) {
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+                               } else {
+                                       phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
+                               }
+                       } else if (phrase.startsWith("-")) {
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+                               } else {
+                                       phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
+                               }
+                       } else {
+                               phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
+                       }
+               }
+               return phrases;
+       }
+
+       /**
+        * Calculates the score for the given expression when using the given
+        * phrases.
+        *
+        * @param phrases
+        *            The phrases to search for
+        * @param expression
+        *            The expression to search
+        * @return The score of the expression
+        */
+       private static double calculateScore(List<Phrase> phrases, String expression) {
+               logger.log(Level.FINEST, String.format("Calculating Score for “%s”…", expression));
+               double optionalHits = 0;
+               double requiredHits = 0;
+               int forbiddenHits = 0;
+               int requiredPhrases = 0;
+               for (Phrase phrase : phrases) {
+                       String phraseString = phrase.getPhrase().toLowerCase();
+                       if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
+                               ++requiredPhrases;
+                       }
+                       int matches = 0;
+                       int index = 0;
+                       double score = 0;
+                       while (index < expression.length()) {
+                               int position = expression.toLowerCase().indexOf(phraseString, index);
+                               if (position == -1) {
+                                       break;
+                               }
+                               score += Math.pow(1 - position / (double) expression.length(), 2);
+                               index = position + phraseString.length();
+                               logger.log(Level.FINEST, String.format("Got hit at position %d.", position));
+                               ++matches;
+                       }
+                       logger.log(Level.FINEST, String.format("Score: %f", score));
+                       if (matches == 0) {
+                               continue;
+                       }
+                       if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
+                               requiredHits += score;
+                       }
+                       if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
+                               optionalHits += score;
+                       }
+                       if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
+                               forbiddenHits += matches;
+                       }
+               }
+               return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2);
+       }
+
+       /**
+        * Throws a
+        * {@link net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException}
+        * if the given object is not {@code null}, appending the object to the
+        * given target URL.
+        *
+        * @param object
+        *            The object on which to redirect
+        * @param target
+        *            The target of the redirect
+        * @throws RedirectException
+        *             if {@code object} is not {@code null}
+        */
+       private static void redirectIfNotNull(String object, String target) throws RedirectException {
+               if (object != null) {
+                       throw new RedirectException(target + object);
+               }
+       }
+
+       /**
+        * If the given phrase contains a Sone ID (optionally prefixed by
+        * “sone://”), returns said Sone ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a Sone ID
+        * @return The Sone ID, or {@code null}
+        */
+       private String getSoneId(String phrase) {
+               String soneId = phrase.startsWith("sone://") ? phrase.substring(7) : phrase;
+               return (webInterface.getCore().getSone(soneId).isPresent()) ? soneId : null;
+       }
+
+       /**
+        * If the given phrase contains a post ID (optionally prefixed by
+        * “post://”), returns said post ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a post ID
+        * @return The post ID, or {@code null}
+        */
+       private String getPostId(String phrase) {
+               String postId = phrase.startsWith("post://") ? phrase.substring(7) : phrase;
+               return (webInterface.getCore().getPost(postId).isPresent()) ? postId : null;
+       }
+
+       /**
+        * If the given phrase contains a reply ID (optionally prefixed by
+        * “reply://”), returns the ID of the post the reply belongs to, otherwise
+        * return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a reply ID
+        * @return The reply’s post ID, or {@code null}
+        */
+       private String getReplyPostId(String phrase) {
+               String replyId = phrase.startsWith("reply://") ? phrase.substring(8) : phrase;
+               Optional<PostReply> postReply = webInterface.getCore().getPostReply(replyId);
+               if (!postReply.isPresent()) {
+                       return null;
+               }
+               return postReply.get().getPostId();
+       }
+
+       /**
+        * If the given phrase contains an album ID (optionally prefixed by
+        * “album://”), returns said album ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is an album ID
+        * @return The album ID, or {@code null}
+        */
+       private String getAlbumId(String phrase) {
+               String albumId = phrase.startsWith("album://") ? phrase.substring(8) : phrase;
+               return (webInterface.getCore().getAlbum(albumId) != null) ? albumId : null;
+       }
+
+       /**
+        * If the given phrase contains an image ID (optionally prefixed by
+        * “image://”), returns said image ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is an image ID
+        * @return The image ID, or {@code null}
+        */
+       private String getImageId(String phrase) {
+               String imageId = phrase.startsWith("image://") ? phrase.substring(8) : phrase;
+               return (webInterface.getCore().getImage(imageId, false) != null) ? imageId : null;
+       }
+
+       /**
+        * Converts a given object into a {@link String}.
+        *
+        * @param <T>
+        *            The type of the objects
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static interface StringGenerator<T> {
+
+               /**
+                * Generates a {@link String} for the given object.
+                *
+                * @param object
+                *            The object to generate the {@link String} for
+                * @return The generated {@link String}
+                */
+               public String generateString(T object);
+
+       }
+
+       /**
+        * Generates a {@link String} from a {@link Sone}, concatenating the name of
+        * the Sone and all {@link Profile} {@link Field} values.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class SoneStringGenerator implements StringGenerator<Sone> {
+
+               /** A static instance of a complete Sone string generator. */
+               public static final SoneStringGenerator COMPLETE_GENERATOR = new SoneStringGenerator(true);
+
+               /**
+                * A static instance of a Sone string generator that will only use the
+                * name of the Sone.
+                */
+               public static final SoneStringGenerator NAME_GENERATOR = new SoneStringGenerator(false);
+
+               /** Whether to generate a string from all data of a Sone. */
+               private final boolean complete;
+
+               /**
+                * Creates a new Sone string generator.
+                *
+                * @param complete
+                *            {@code true} to use the profile’s fields, {@code false} to
+                *            not to use the profile‘s fields
+                */
+               private SoneStringGenerator(boolean complete) {
+                       this.complete = complete;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String generateString(Sone sone) {
+                       StringBuilder soneString = new StringBuilder();
+                       soneString.append(sone.getName());
+                       Profile soneProfile = sone.getProfile();
+                       if (soneProfile.getFirstName() != null) {
+                               soneString.append(' ').append(soneProfile.getFirstName());
+                       }
+                       if (soneProfile.getMiddleName() != null) {
+                               soneString.append(' ').append(soneProfile.getMiddleName());
+                       }
+                       if (soneProfile.getLastName() != null) {
+                               soneString.append(' ').append(soneProfile.getLastName());
+                       }
+                       if (complete) {
+                               for (Field field : soneProfile.getFields()) {
+                                       soneString.append(' ').append(field.getValue());
+                               }
+                       }
+                       return soneString.toString();
+               }
+
+       }
+
+       /**
+        * Generates a {@link String} from a {@link Post}, concatenating the text of
+        * the post, the text of all {@link Reply}s, and the name of all
+        * {@link Sone}s that have replied.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private class PostStringGenerator implements StringGenerator<Post> {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String generateString(Post post) {
+                       StringBuilder postString = new StringBuilder();
+                       postString.append(post.getText());
+                       if (post.getRecipient().isPresent()) {
+                               postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient().get()));
+                       }
+                       for (PostReply reply : Collections2.filter(webInterface.getCore().getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER)) {
+                               postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(reply.getSone()));
+                               postString.append(' ').append(reply.getText());
+                       }
+                       return postString.toString();
+               }
+
+       }
+
+       /**
+        * A search phrase.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class Phrase {
+
+               /**
+                * The optionality of a search phrase.
+                *
+                * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
+                *         Roden</a>
+                */
+               public enum Optionality {
+
+                       /** The phrase is optional. */
+                       OPTIONAL,
+
+                       /** The phrase is required. */
+                       REQUIRED,
+
+                       /** The phrase is forbidden. */
+                       FORBIDDEN
+
+               }
+
+               /** The phrase to search for. */
+               private final String phrase;
+
+               /** The optionality of the phrase. */
+               private final Optionality optionality;
+
+               /**
+                * Creates a new phrase.
+                *
+                * @param phrase
+                *            The phrase to search for
+                * @param optionality
+                *            The optionality of the phrase
+                */
+               public Phrase(String phrase, Optionality optionality) {
+                       this.optionality = optionality;
+                       this.phrase = phrase;
+               }
+
+               /**
+                * Returns the phrase to search for.
+                *
+                * @return The phrase to search for
+                */
+               public String getPhrase() {
+                       return phrase;
+               }
+
+               /**
+                * Returns the optionality of the phrase.
+                *
+                * @return The optionality of the phrase
+                */
+               public Optionality getOptionality() {
+                       return optionality;
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return phrase.hashCode() ^ ((optionality == Optionality.FORBIDDEN) ? (0xaaaaaaaa) : ((optionality == Optionality.REQUIRED) ? 0x55555555 : 0));
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Phrase)) {
+                               return false;
+                       }
+                       Phrase phrase = (Phrase) object;
+                       return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
+               }
+
+       }
+
+       /**
+        * A hit consists of a searched object and the score it got for the phrases
+        * of the search.
+        *
+        * @see SearchPage#calculateScore(List, String)
+        * @param <T>
+        *            The type of the searched object
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class Hit<T> {
+
+               /** Filter for {@link Hit}s with a score of more than 0. */
+               public static final Predicate<Hit<?>> POSITIVE_FILTER = new Predicate<Hit<?>>() {
+
+                       @Override
+                       public boolean apply(Hit<?> hit) {
+                               return (hit != null) && (hit.getScore() > 0);
+                       }
+
+               };
+
+               /** Comparator that sorts {@link Hit}s descending by score. */
+               public static final Comparator<Hit<?>> DESCENDING_COMPARATOR = new Comparator<Hit<?>>() {
+
+                       @Override
+                       public int compare(Hit<?> leftHit, Hit<?> rightHit) {
+                               return Double.compare(rightHit.getScore(), leftHit.getScore());
+                       }
+
+               };
+
+               /** The object that was searched. */
+               private final T object;
+
+               /** The score of the object. */
+               private final double score;
+
+               /**
+                * Creates a new hit.
+                *
+                * @param object
+                *            The object that was searched
+                * @param score
+                *            The score of the object
+                */
+               public Hit(T object, double score) {
+                       this.object = object;
+                       this.score = score;
+               }
+
+               /**
+                * Returns the object that was searched.
+                *
+                * @return The object that was searched
+                */
+               public T getObject() {
+                       return object;
+               }
+
+               /**
+                * Returns the score of the object.
+                *
+                * @return The score of the object
+                */
+               public double getScore() {
+                       return score;
+               }
+
+       }
+
+       /**
+        * Extracts the object from a {@link Hit}.
+        *
+        * @param <T>
+        *            The type of the object to extract
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class HitMapper<T> implements Function<Hit<T>, T> {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public T apply(Hit<T> input) {
+                       return input.getObject();
+               }
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/pages/SoneTemplatePage.java
new file mode 100644 (file)
index 0000000..46c8318
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * Sone - SoneTemplatePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.sone.web.page.FreenetTemplatePage;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+import freenet.clients.http.SessionManager.Session;
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Base page for the Sone web interface.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTemplatePage extends FreenetTemplatePage {
+
+       /** The Sone core. */
+       protected final WebInterface webInterface;
+
+       /** The page title l10n key. */
+       private final String pageTitleKey;
+
+       /** Whether to require a login. */
+       private final boolean requireLogin;
+
+       /**
+        * Creates a new template page for Sone that does not require the user to be
+        * logged in.
+        *
+        * @param path
+        *            The path of the page
+        * @param template
+        *            The template to render
+        * @param pageTitleKey
+        *            The l10n key of the page title
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface) {
+               this(path, template, pageTitleKey, webInterface, false);
+       }
+
+       /**
+        * Creates a new template page for Sone.
+        *
+        * @param path
+        *            The path of the page
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        * @param requireLogin
+        *            Whether this page requires a login
+        */
+       public SoneTemplatePage(String path, Template template, WebInterface webInterface, boolean requireLogin) {
+               this(path, template, null, webInterface, requireLogin);
+       }
+
+       /**
+        * Creates a new template page for Sone.
+        *
+        * @param path
+        *            The path of the page
+        * @param template
+        *            The template to render
+        * @param pageTitleKey
+        *            The l10n key of the page title
+        * @param webInterface
+        *            The Sone web interface
+        * @param requireLogin
+        *            Whether this page requires a login
+        */
+       public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface, boolean requireLogin) {
+               super(path, webInterface.getTemplateContextFactory(), template, "noPermission.html");
+               this.pageTitleKey = pageTitleKey;
+               this.webInterface = webInterface;
+               this.requireLogin = requireLogin;
+       }
+
+       //
+       // PROTECTED METHODS
+       //
+
+       /**
+        * 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
+        */
+       protected Sone getCurrentSone(ToadletContext toadletContext) {
+               return webInterface.getCurrentSoneCreatingSession(toadletContext);
+       }
+
+       protected Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
+               return webInterface.getCurrentSoneWithoutCreatingSession(toadletContext);
+       }
+
+       /**
+        * Sets the currently logged in Sone.
+        *
+        * @param toadletContext
+        *            The toadlet context
+        * @param sone
+        *            The Sone to set as currently logged in
+        */
+       protected void setCurrentSone(ToadletContext toadletContext, Sone sone) {
+               webInterface.setCurrentSone(toadletContext, sone);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected String getPageTitle(FreenetRequest request) {
+               if (pageTitleKey != null) {
+                       return webInterface.getL10n().getString(pageTitleKey);
+               }
+               return "";
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
+               return ImmutableList.<Map<String, String>> builder().add(ImmutableMap.<String, String> builder().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").build()).build();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected Collection<String> getStyleSheets() {
+               return Arrays.asList("css/sone.css");
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected String getShortcutIcon() {
+               return "images/icon.png";
+       }
+
+       /**
+        * Returns whether this page requires the user to log in.
+        *
+        * @return {@code true} if the user is required to be logged in to use this
+        *         page, {@code false} otherwise
+        */
+       protected boolean requiresLogin() {
+               return requireLogin;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected final void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Sone currentSone = getCurrentSoneWithoutCreatingSession(request.getToadletContext());
+               templateContext.set("preferences", webInterface.getCore().getPreferences());
+               templateContext.set("currentSone", currentSone);
+               templateContext.set("localSones", webInterface.getCore().getLocalSones());
+               templateContext.set("request", request);
+               templateContext.set("currentVersion", SonePlugin.getPluginVersion());
+               templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+               templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+               templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+               templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
+               Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
+               templateContext.set("notifications", notifications);
+               templateContext.set("notificationHash", notifications.hashCode());
+               handleRequest(request, templateContext);
+       }
+
+       protected void handleRequest(@Nonnull FreenetRequest request, @Nonnull TemplateContext templateContext) throws RedirectException {
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected String getRedirectTarget(FreenetRequest request) {
+               if (requiresLogin() && (getCurrentSoneWithoutCreatingSession(request.getToadletContext()) == null)) {
+                       HTTPRequest httpRequest = request.getHttpRequest();
+                       String originalUrl = httpRequest.getPath();
+                       if (httpRequest.hasParameters()) {
+                               StringBuilder requestParameters = new StringBuilder();
+                               for (String parameterName : httpRequest.getParameterNames()) {
+                                       if (requestParameters.length() > 0) {
+                                               requestParameters.append("&");
+                                       }
+                                       String[] parameterValues = httpRequest.getMultipleParam(parameterName);
+                                       for (String parameterValue : parameterValues) {
+                                               requestParameters.append(urlEncode(parameterName)).append("=").append(urlEncode(parameterValue));
+                                       }
+                               }
+                               originalUrl += "?" + requestParameters.toString();
+                       }
+                       return "login.html?target=" + urlEncode(originalUrl);
+               }
+               return null;
+       }
+
+       private static String urlEncode(String value) {
+               try {
+                       return URLEncoder.encode(value, "UTF-8");
+               } catch (UnsupportedEncodingException uee1) {
+                                                       /* A JVM without UTF-8? I don’t think so. */
+                       throw new RuntimeException(uee1);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean isFullAccessOnly() {
+               return webInterface.getCore().getPreferences().isRequireFullAccess();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
+               if (requiresLogin()) {
+                       return getCurrentSoneWithoutCreatingSession(toadletContext) != null;
+               }
+               return true;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/TrustPage.java b/src/main/java/net/pterodactylus/sone/web/pages/TrustPage.java
new file mode 100644 (file)
index 0000000..55929d7
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - TrustPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import com.google.common.base.Optional;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user trust another Sone. This will assign a configurable
+ * amount of trust to an identity.
+ *
+ * @see Core#trustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TrustPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “trust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public TrustPage(Template template, WebInterface webInterface) {
+               super("trust.html", template, "Page.Trust.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Optional<Sone> sone = webInterface.getCore().getSone(identity);
+                       if (sone.isPresent()) {
+                               webInterface.getCore().trustSone(currentSone, sone.get());
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UnbookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/pages/UnbookmarkPage.java
new file mode 100644 (file)
index 0000000..6fa037f
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - UnbookmarkPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+import com.google.common.base.Optional;
+
+/**
+ * Page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnbookmarkPage(Template template, WebInterface webInterface) {
+               super("unbookmark.html", template, "Page.Unbookmark.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Optional<Post> post = webInterface.getCore().getPost(id);
+                       if (post.isPresent()) {
+                               webInterface.getCore().unbookmarkPost(post.get());
+                       }
+                       throw new RedirectException(returnPage);
+               }
+               String id = request.getHttpRequest().getParam("post");
+               if (id.equals("allNotLoaded")) {
+                       Set<Post> posts = webInterface.getCore().getBookmarkedPosts();
+                       for (Post post : posts) {
+                               if (!post.isLoaded()) {
+                                       webInterface.getCore().unbookmarkPost(post);
+                               }
+                       }
+                       throw new RedirectException("bookmarks.html");
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UnfollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/pages/UnfollowSonePage.java
new file mode 100644 (file)
index 0000000..ec377e2
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sone - UnfollowSonePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * This page lets the user unfollow another Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnfollowSonePage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnfollowSonePage(Template template, WebInterface webInterface) {
+               super("unfollowSone.html", template, "Page.UnfollowSone.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               webInterface.getCore().unfollowSone(currentSone, soneId);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UnlikePage.java b/src/main/java/net/pterodactylus/sone/web/pages/UnlikePage.java
new file mode 100644 (file)
index 0000000..9e61c4f
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Sone - UnlikePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user unlike a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnlikePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “unlike post” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnlikePage(Template template, WebInterface webInterface) {
+               super("unlike.html", template, "Page.Unlike.Title", webInterface, true);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+                       String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       if ("post".equals(type)) {
+                               currentSone.removeLikedPostId(id);
+                       } else if ("reply".equals(type)) {
+                               currentSone.removeLikedReplyId(id);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UnlockSonePage.java b/src/main/java/net/pterodactylus/sone/web/pages/UnlockSonePage.java
new file mode 100644 (file)
index 0000000..960f486
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sone - UnlockSonePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * This page lets the user unlock a {@link Sone} to allow its insertion.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnlockSonePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “unlock Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnlockSonePage(Template template, WebInterface webInterface) {
+               super("unlockSone.html", template, "Page.UnlockSone.Title", webInterface);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+               Sone sone = webInterface.getCore().getLocalSone(soneId);
+               if (sone != null) {
+                       webInterface.getCore().unlockSone(sone);
+               }
+               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+               throw new RedirectException(returnPage);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UntrustPage.java b/src/main/java/net/pterodactylus/sone/web/pages/UntrustPage.java
new file mode 100644 (file)
index 0000000..de0b086
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - UntrustPage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import com.google.common.base.Optional;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user untrust another Sone. This will remove all trust
+ * assignments for an identity.
+ *
+ * @see Core#untrustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UntrustPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “untrust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UntrustPage(Template template, WebInterface webInterface) {
+               super("untrust.html", template, "Page.Untrust.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Optional<Sone> sone = webInterface.getCore().getSone(identity);
+                       if (sone.isPresent()) {
+                               webInterface.getCore().untrustSone(currentSone, sone.get());
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/UploadImagePage.java b/src/main/java/net/pterodactylus/sone/web/pages/UploadImagePage.java
new file mode 100644 (file)
index 0000000..6b77132
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * Sone - UploadImagePage.java - Copyright © 2011–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static com.google.common.base.Optional.fromNullable;
+import static java.util.logging.Logger.getLogger;
+
+import java.awt.Image;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image.Modifier.ImageTitleMustNotBeEmpty;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+import com.google.common.io.ByteStreams;
+
+import freenet.support.api.Bucket;
+import freenet.support.api.HTTPUploadedFile;
+
+/**
+ * Page implementation that lets the user upload an image.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UploadImagePage extends SoneTemplatePage {
+
+       private static final Logger logger = getLogger(UploadImagePage.class.getName());
+       private static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
+
+       /**
+        * Creates a new “upload image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UploadImagePage(Template template, WebInterface webInterface) {
+               super("uploadImage.html", template, "Page.UploadImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               if (request.getMethod() == Method.POST) {
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
+                       Album parent = webInterface.getCore().getAlbum(parentId);
+                       if (parent == null) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if (!currentSone.equals(parent.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("title", 200).trim();
+                       if (name.length() == 0) {
+                               throw new RedirectException("emptyImageTitle.html");
+                       }
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 4000);
+                       HTTPUploadedFile uploadedFile = request.getHttpRequest().getUploadedFile("image");
+                       Bucket fileBucket = uploadedFile.getData();
+                       InputStream imageInputStream = null;
+                       ByteArrayOutputStream imageDataOutputStream = null;
+                       try {
+                               imageInputStream = fileBucket.getInputStream();
+                               /* TODO - check length */
+                               imageDataOutputStream = new ByteArrayOutputStream((int) fileBucket.size());
+                               ByteStreams.copy(imageInputStream, imageDataOutputStream);
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } finally {
+                               fileBucket.free();
+                               Closer.close(imageInputStream);
+                               Closer.close(imageDataOutputStream);
+                       }
+                       byte[] imageData = imageDataOutputStream.toByteArray();
+                       ByteArrayInputStream imageDataInputStream = null;
+                       Image uploadedImage = null;
+                       try {
+                               imageDataInputStream = new ByteArrayInputStream(imageData);
+                               uploadedImage = ImageIO.read(imageDataInputStream);
+                               if (uploadedImage == null) {
+                                       templateContext.set("messages", webInterface.getL10n().getString("Page.UploadImage.Error.InvalidImage"));
+                                       return;
+                               }
+                               String mimeType = getMimeType(imageData);
+                               TemporaryImage temporaryImage = webInterface.getCore().createTemporaryImage(mimeType, imageData);
+                               net.pterodactylus.sone.data.Image image = webInterface.getCore().createImage(currentSone, parent, temporaryImage);
+                               image.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null)).update();
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } catch (ImageTitleMustNotBeEmpty itmnbe) {
+                               throw new RedirectException("emptyImageTitle.html");
+                       } finally {
+                               Closer.close(imageDataInputStream);
+                               Closer.flush(uploadedImage);
+                       }
+                       throw new RedirectException("imageBrowser.html?album=" + parent.getId());
+               }
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Tries to detect the MIME type of the encoded image.
+        *
+        * @param imageData
+        *            The encoded image
+        * @return The MIME type of the image, or “application/octet-stream” if the
+        *         image type could not be detected
+        */
+       private static String getMimeType(byte[] imageData) {
+               ByteArrayInputStream imageDataInputStream = new ByteArrayInputStream(imageData);
+               try {
+                       ImageInputStream imageInputStream = ImageIO.createImageInputStream(imageDataInputStream);
+                       Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
+                       if (imageReaders.hasNext()) {
+                               return fromNullable(imageReaders.next().getOriginatingProvider().getMIMETypes())
+                                               .or(new String[] { UNKNOWN_MIME_TYPE })[0];
+                       }
+               } catch (IOException ioe1) {
+                       logger.log(Level.FINE, "Could not detect MIME type for image.", ioe1);
+               }
+               return UNKNOWN_MIME_TYPE;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/ViewPostPage.java b/src/main/java/net/pterodactylus/sone/web/pages/ViewPostPage.java
new file mode 100644 (file)
index 0000000..12d6be9
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sone - ViewPostPage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import java.net.URI;
+
+import com.google.common.base.Optional;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * This page lets the user view a post and all its replies.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ViewPostPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “view post” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public ViewPostPage(Template template, WebInterface webInterface) {
+               super("viewPost.html", template, "Page.ViewPost.Title", webInterface, false);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected String getPageTitle(FreenetRequest request) {
+               String postId = request.getHttpRequest().getParam("post");
+               Optional<Post> post = webInterface.getCore().getPost(postId);
+               String title = "";
+               if (post.isPresent()) {
+                       title = post.get().getText().substring(0, Math.min(20, post.get().getText().length())) + "…";
+                       title += " - " + SoneAccessor.getNiceName(post.get().getSone()) + " - ";
+               }
+               title += webInterface.getL10n().getString("Page.ViewPost.Title");
+               return title;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String postId = request.getHttpRequest().getParam("post");
+               boolean raw = request.getHttpRequest().getParam("raw").equals("true");
+               Optional<Post> post = webInterface.getCore().getPost(postId);
+               templateContext.set("post", post.orNull());
+               templateContext.set("raw", raw);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/pages/ViewSonePage.java b/src/main/java/net/pterodactylus/sone/web/pages/ViewSonePage.java
new file mode 100644 (file)
index 0000000..257d2b2
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Sone - ViewSonePage.java - Copyright © 2010–2016 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+import com.google.common.base.Optional;
+
+/**
+ * Lets the user browser another Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ViewSonePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “view Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public ViewSonePage(Template template, WebInterface webInterface) {
+               super("viewSone.html", template, webInterface, false);
+       }
+
+       //
+       // TEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected String getPageTitle(FreenetRequest request) {
+               String soneId = request.getHttpRequest().getParam("sone");
+               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
+               if (sone.isPresent()) {
+                       String soneName = SoneAccessor.getNiceName(sone.get());
+                       return soneName + " - " + webInterface.getL10n().getString("Page.ViewSone.Title");
+               }
+               return webInterface.getL10n().getString("Page.ViewSone.Page.TitleWithoutSone");
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               String soneId = request.getHttpRequest().getParam("sone");
+               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
+               templateContext.set("sone", sone.orNull());
+               templateContext.set("soneId", soneId);
+               if (!sone.isPresent()) {
+                       return;
+               }
+               List<Post> sonePosts = sone.get().getPosts();
+               sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone.get().getId()));
+               Collections.sort(sonePosts, Post.NEWEST_FIRST);
+               Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("postPage"), 0));
+               templateContext.set("postPagination", postPagination);
+               templateContext.set("posts", postPagination.getItems());
+               Set<PostReply> replies = sone.get().getReplies();
+               final Map<Post, List<PostReply>> repliedPosts = new HashMap<Post, List<PostReply>>();
+               for (PostReply reply : replies) {
+                       Optional<Post> post = reply.getPost();
+                       if (!post.isPresent() || repliedPosts.containsKey(post.get()) || sone.get().equals(post.get().getSone()) || (sone.get().getId().equals(post.get().getRecipientId().orNull()))) {
+                               continue;
+                       }
+                       repliedPosts.put(post.get(), webInterface.getCore().getReplies(post.get().getId()));
+               }
+               List<Post> posts = new ArrayList<Post>(repliedPosts.keySet());
+               Collections.sort(posts, new Comparator<Post>() {
+
+                       @Override
+                       public int compare(Post leftPost, Post rightPost) {
+                               return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, repliedPosts.get(rightPost).get(0).getTime() - repliedPosts.get(leftPost).get(0).getTime()));
+                       }
+
+               });
+
+               Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("repliedPostPage"), 0));
+               templateContext.set("repliedPostPagination", repliedPostPagination);
+               templateContext.set("repliedPosts", repliedPostPagination.getItems());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/AboutPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/AboutPage.kt
deleted file mode 100644 (file)
index da60e98..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.main.SonePlugin.PluginHomepage
-import net.pterodactylus.sone.main.SonePlugin.PluginVersion
-import net.pterodactylus.sone.main.SonePlugin.PluginYear
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * A [SoneTemplatePage] that stores information about Sone in the [TemplateContext].
- */
-class AboutPage(template: Template, webInterface: WebInterface,
-               private val pluginVersion: PluginVersion,
-               private val pluginYear: PluginYear,
-               private val pluginHomepage: PluginHomepage): SoneTemplatePage("about.html", template, "Page.About.Title", webInterface, false) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               templateContext["version"] = pluginVersion.version
-               templateContext["year"] = pluginYear.year
-               templateContext["homepage"] = pluginHomepage.homepage
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/BookmarkPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/BookmarkPage.kt
deleted file mode 100644 (file)
index 5bc21d1..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user bookmark a post.
- */
-class BookmarkPage(template: Template, webInterface: WebInterface)
-       : SoneTemplatePage("bookmark.html", template, "Page.Bookmark.Title", webInterface) {
-
-       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
-               if (freenetRequest.isPOST) {
-                       val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
-                       val postId = freenetRequest.httpRequest.getPartAsStringFailsafe("post", 36)
-                       webInterface.core.getPost(postId).orNull()?.let {
-                               webInterface.core.bookmarkPost(it)
-                       }
-                       throw RedirectException(returnPage)
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/BookmarksPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/BookmarksPage.kt
deleted file mode 100644 (file)
index 25d3317..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.collection.Pagination
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user browse all his bookmarked posts.
- */
-class BookmarksPage(template: Template, webInterface: WebInterface): SoneTemplatePage("bookmarks.html", template, "Page.Bookmarks.Title", webInterface) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               webInterface.core.bookmarkedPosts.let { posts ->
-                       val pagination = Pagination<Post>(posts.filter { it.isLoaded }.sortedByDescending { it.time }, webInterface.core.preferences.postsPerPage)
-                       templateContext["pagination"] = pagination
-                       templateContext["posts"] = pagination.items
-                       templateContext["postsNotLoaded"] = posts.any { !it.isLoaded }
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/CreatePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/CreatePostPage.kt
deleted file mode 100644 (file)
index 302fdf5..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.text.TextFilter
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * This page lets the user create a new [Post].
- */
-class CreatePostPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("createPost.html", template, "Page.CreatePost.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
-               templateContext["returnPage"] = returnPage
-               if (request.isPOST) {
-                       val text = request.httpRequest.getPartAsStringFailsafe("text", 65536).trim()
-                       if (text == "") {
-                               templateContext["errorTextEmpty"] = true
-                               return
-                       }
-                       val sender = webInterface.core.getLocalSone(request.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(request.toadletContext)
-                       val recipient = webInterface.core.getSone(request.httpRequest.getPartAsStringFailsafe("recipient", 43))
-                       webInterface.core.createPost(sender, recipient, TextFilter.filter(request.httpRequest.getHeader("Host"), text))
-                       throw RedirectException(returnPage)
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/CreateSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/CreateSonePage.kt
deleted file mode 100644 (file)
index 673cca3..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.pterodactylus.sone.web
-
-import freenet.clients.http.ToadletContext
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-import java.util.logging.Level
-import java.util.logging.Logger
-
-/**
- * The “create Sone” page lets the user create a new Sone.
- */
-class CreateSonePage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("createSone.html", template, "Page.CreateSone.Title", webInterface, false) {
-
-       private val logger = Logger.getLogger(CreateSonePage::class.java.name)
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               templateContext["sones"] = webInterface.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
-               templateContext["identitiesWithoutSone"] = webInterface.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}".toLowerCase() }
-               if (request.isPOST) {
-                       val identity = request.httpRequest.getPartAsStringFailsafe("identity", 43)
-                       webInterface.core.identityManager.allOwnIdentities.firstOrNull { it.id == identity }?.let { ownIdentity ->
-                               val sone = webInterface.core.createSone(ownIdentity)
-                               if (sone == null) {
-                                       logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: $ownIdentity")
-                               }
-                               setCurrentSone(request.toadletContext, sone)
-                               throw RedirectException("index.html")
-                       }
-                       templateContext["errorNoIdentity"] = true
-               }
-       }
-
-       override fun isEnabled(toadletContext: ToadletContext) =
-                       if (webInterface.core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess) {
-                               false
-                       } else {
-                               (getCurrentSone(toadletContext) == null) || (webInterface.core.localSones.size == 1)
-                       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeleteAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeleteAlbumPage.kt
deleted file mode 100644 (file)
index 7830653..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user delete an {@link Album}.
- */
-class DeleteAlbumPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deleteAlbum.html", template, "Page.DeleteAlbum.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val album = webInterface.core.getAlbum(request.httpRequest.getPartAsStringFailsafe("album", 36))
-               templateContext["album"] = album ?: throw RedirectException("invalid.html")
-               if (request.isPOST) {
-                       if (!album.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
-                       }
-                       if (request.httpRequest.getPartAsStringFailsafe("abortDelete", 4) == "true") {
-                               throw RedirectException("imageBrowser.html?album=${album.id}")
-                       }
-                       webInterface.core.deleteAlbum(album)
-                       throw RedirectException(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}")
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeleteImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeleteImagePage.kt
deleted file mode 100644 (file)
index 00ebc55..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user delete an {@link Image}.
- */
-class DeleteImagePage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deleteImage.html", template, "Page.DeleteImage.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val image = webInterface.core.getImage(request.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
-               if (!image.sone.isLocal) {
-                       throw RedirectException("noPermission.html")
-               }
-               if (request.isPOST) {
-                       if (request.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException("imageBrowser.html?image=${image.id}")
-                       }
-                       webInterface.core.deleteImage(image)
-                       throw RedirectException("imageBrowser.html?album=${image.album.id}")
-               }
-               templateContext["image"] = image
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeletePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeletePostPage.kt
deleted file mode 100644 (file)
index bbbb897..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Lets the user delete a post they made.
- */
-class DeletePostPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deletePost.html", template, "Page.DeletePost.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val post = webInterface.core.getPost(request.httpRequest.getPartAsStringFailsafe("post", 36)).orNull() ?: throw RedirectException("noPermission.html")
-               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
-               if (request.isPOST) {
-                       if (!post.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
-                       }
-                       if (request.httpRequest.isPartSet("confirmDelete")) {
-                               webInterface.core.deletePost(post)
-                               throw RedirectException(returnPage)
-                       } else if (request.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException(returnPage)
-                       }
-               }
-               templateContext["post"] = post
-               templateContext["returnPage"] = returnPage
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPage.kt
deleted file mode 100644 (file)
index c957bd7..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user confirm the deletion of a profile field.
- */
-class DeleteProfileFieldPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deleteProfileField.html", template, "Page.DeleteProfileField.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val currentSone = getCurrentSone(request.toadletContext)
-               val field = currentSone.profile.getFieldById(request.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
-               templateContext["field"] = field
-               if (request.isPOST) {
-                       if (request.httpRequest.getPartAsStringFailsafe("confirm", 4) == "true") {
-                               currentSone.profile = currentSone.profile.apply { removeField(field) }
-                       }
-                       throw RedirectException("editProfile.html#profile-fields")
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeleteReplyPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeleteReplyPage.kt
deleted file mode 100644 (file)
index 5a707ce..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * This page lets the user delete a reply.
- */
-class DeleteReplyPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deleteReply.html", template, "Page.DeleteReply.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val replyId = request.httpRequest.getPartAsStringFailsafe("reply", 36)
-               templateContext["reply"] = replyId
-               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
-               templateContext["returnPage"] = returnPage
-               if (request.isPOST) {
-                       val reply = webInterface.core.getPostReply(replyId).orNull() ?: throw RedirectException("noPermission.html")
-                       if (!reply.sone.isLocal) {
-                               throw RedirectException("noPermission.html")
-                       }
-                       if (request.httpRequest.isPartSet("confirmDelete")) {
-                               webInterface.core.deleteReply(reply)
-                               throw RedirectException(returnPage)
-                       }
-                       if (request.httpRequest.isPartSet("abortDelete")) {
-                               throw RedirectException(returnPage)
-                       }
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DeleteSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DeleteSonePage.kt
deleted file mode 100644 (file)
index 1d0f6aa..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Lets the user delete a Sone. Of course the Sone is not really deleted from
- * Freenet; merely all references to it are removed from the local plugin
- * installation.
- */
-class DeleteSonePage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("deleteSone.html", template, "Page.DeleteSone.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               if (request.isPOST) {
-                       if (request.httpRequest.isPartSet("deleteSone")) {
-                               webInterface.core.deleteSone(getCurrentSone(request.toadletContext))
-                       }
-                       throw RedirectException("index.html")
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DismissNotificationPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DismissNotificationPage.kt
deleted file mode 100644 (file)
index 27bb744..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user dismiss a notification.
- */
-class DismissNotificationPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("dismissNotification.html", template, "Page.DismissNotification.Title", webInterface) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
-               val notificationId = request.httpRequest.getPartAsStringFailsafe("notification", 36)
-               webInterface.getNotification(notificationId).orNull()?.takeIf { it.isDismissable }?.dismiss()
-               throw RedirectException(returnPage)
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/DistrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/DistrustPage.kt
deleted file mode 100644 (file)
index f3e593e..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user distrust another Sone. This will assign a
- * configurable (negative) amount of trust to an identity.
- *
- * @see net.pterodactylus.sone.core.Core#distrustSone(Sone, Sone)
- */
-class DistrustPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("distrust.html", template, "Page.Distrust.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               if (request.isPOST) {
-                       val sone = webInterface.core.getSone(request.httpRequest.getPartAsStringFailsafe("sone", 44)).orNull()
-                       sone?.run { webInterface.core.distrustSone(getCurrentSone(request.toadletContext), this) }
-                       throw RedirectException(request.httpRequest.getPartAsStringFailsafe("returnPage", 256))
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/EditAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/EditAlbumPage.kt
deleted file mode 100644 (file)
index 85ee17a..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Page that lets the user edit the name and description of an album.
- */
-class EditAlbumPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               if (request.isPOST) {
-                       val album = webInterface.core.getAlbum(request.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
-                       album.takeUnless { it.sone.isLocal }?.run { throw RedirectException("noPermission.html") }
-                       if (request.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
-                               album.parent?.moveAlbumUp(album)
-                               webInterface.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
-                       } else if (request.httpRequest.getPartAsStringFailsafe("moveRight", 4) == "true") {
-                               album.parent?.moveAlbumDown(album)
-                               webInterface.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
-                       } else {
-                               try {
-                                       album.modify()
-                                                       .setTitle(request.httpRequest.getPartAsStringFailsafe("title", 100))
-                                                       .setDescription(request.httpRequest.getPartAsStringFailsafe("description", 1000))
-                                                       .update()
-                               } catch (e: AlbumTitleMustNotBeEmpty) {
-                                       throw RedirectException("emptyAlbumTitle.html")
-                               }
-                               webInterface.core.touchConfiguration()
-                               throw RedirectException("imageBrowser.html?album=${album.id}")
-                       }
-               }
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/LoginPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/LoginPage.kt
deleted file mode 100644 (file)
index 302e5e4..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.pterodactylus.sone.web
-
-import freenet.clients.http.ToadletContext
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.utils.isPOST
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.template.Template
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * The login page lets the user log in.
- */
-class LoginPage(template: Template, webInterface: WebInterface):
-               SoneTemplatePage("login.html", template, "Page.Login.Title", webInterface) {
-
-       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-               if (request.isPOST) {
-                       val soneId = request.httpRequest.getPartAsStringFailsafe("sone-id", 43)
-                       webInterface.core.getLocalSone(soneId)?.let { sone ->
-                               setCurrentSone(request.toadletContext, sone)
-                               val target = if (request.httpRequest.isParameterSet("target")) request.httpRequest.getPartAsStringFailsafe("target", 256) else "index.html"
-                               throw RedirectException(target)
-                       }
-               }
-               templateContext["sones"] = webInterface.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
-               templateContext["identitiesWithoutSone"] = webInterface.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}" }
-       }
-
-       override public fun getRedirectTarget(request: FreenetRequest) =
-                       getCurrentSone(request.toadletContext)?.let { "index.html" }
-
-       override fun isEnabled(toadletContext: ToadletContext) = when {
-               webInterface.core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess -> false
-               else -> getCurrentSoneWithoutCreatingSession(toadletContext) == null
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/AboutPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/AboutPage.kt
new file mode 100644 (file)
index 0000000..7809817
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.main.SonePlugin.PluginHomepage
+import net.pterodactylus.sone.main.SonePlugin.PluginVersion
+import net.pterodactylus.sone.main.SonePlugin.PluginYear
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * A [SoneTemplatePage] that stores information about Sone in the [TemplateContext].
+ */
+class AboutPage(template: Template, webInterface: WebInterface,
+               private val pluginVersion: PluginVersion,
+               private val pluginYear: PluginYear,
+               private val pluginHomepage: PluginHomepage): SoneTemplatePage("about.html", template, "Page.About.Title", webInterface, false) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               templateContext["version"] = pluginVersion.version
+               templateContext["year"] = pluginYear.year
+               templateContext["homepage"] = pluginHomepage.homepage
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarkPage.kt
new file mode 100644 (file)
index 0000000..f1e8ac2
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user bookmark a post.
+ */
+class BookmarkPage(template: Template, webInterface: WebInterface)
+       : SoneTemplatePage("bookmark.html", template, "Page.Bookmark.Title", webInterface) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+                       val postId = freenetRequest.httpRequest.getPartAsStringFailsafe("post", 36)
+                       webInterface.core.getPost(postId).orNull()?.let {
+                               webInterface.core.bookmarkPost(it)
+                       }
+                       throw RedirectException(returnPage)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarksPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/BookmarksPage.kt
new file mode 100644 (file)
index 0000000..757ae75
--- /dev/null
@@ -0,0 +1,25 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.collection.Pagination
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user browse all his bookmarked posts.
+ */
+class BookmarksPage(template: Template, webInterface: WebInterface): SoneTemplatePage("bookmarks.html", template, "Page.Bookmarks.Title", webInterface) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               webInterface.core.bookmarkedPosts.let { posts ->
+                       val pagination = Pagination<Post>(posts.filter { it.isLoaded }.sortedByDescending { it.time }, webInterface.core.preferences.postsPerPage)
+                       templateContext["pagination"] = pagination
+                       templateContext["posts"] = pagination.items
+                       templateContext["postsNotLoaded"] = posts.any { !it.isLoaded }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPage.kt
new file mode 100644 (file)
index 0000000..3a628dd
--- /dev/null
@@ -0,0 +1,43 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user create a new album.
+ */
+class CreateAlbumPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("createAlbum.html", template, "Page.CreateAlbum.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               if (request.isPOST) {
+                       val name = request.httpRequest.getPartAsStringFailsafe("name", 64).trim()
+                       if (name.isEmpty()) {
+                               templateContext["nameMissing"] = true
+                               return
+                       }
+                       val description = request.httpRequest.getPartAsStringFailsafe("description", 256).trim()
+                       val currentSone = webInterface.getCurrentSoneCreatingSession(request.toadletContext)
+                       val parentId = request.httpRequest.getPartAsStringFailsafe("parent", 36)
+                       val parent = if (parentId == "") currentSone.rootAlbum else webInterface.core.getAlbum(parentId)
+                       val album = webInterface.core.createAlbum(currentSone, parent)
+                       try {
+                               album.modify().apply {
+                                       setTitle(name)
+                                       setDescription(TextFilter.filter(request.httpRequest.getHeader("Host"), description))
+                               }.update()
+                       } catch (e: AlbumTitleMustNotBeEmpty) {
+                               throw RedirectException("emptyAlbumTitle.html")
+                       }
+                       webInterface.core.touchConfiguration()
+                       throw RedirectException("imageBrowser.html?album=${album.id}")
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreatePostPage.kt
new file mode 100644 (file)
index 0000000..6e39d49
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * This page lets the user create a new [Post].
+ */
+class CreatePostPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("createPost.html", template, "Page.CreatePost.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               templateContext["returnPage"] = returnPage
+               if (request.isPOST) {
+                       val text = request.httpRequest.getPartAsStringFailsafe("text", 65536).trim()
+                       if (text == "") {
+                               templateContext["errorTextEmpty"] = true
+                               return
+                       }
+                       val sender = webInterface.core.getLocalSone(request.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(request.toadletContext)
+                       val recipient = webInterface.core.getSone(request.httpRequest.getPartAsStringFailsafe("recipient", 43))
+                       webInterface.core.createPost(sender, recipient, TextFilter.filter(request.httpRequest.getHeader("Host"), text))
+                       throw RedirectException(returnPage)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPage.kt
new file mode 100644 (file)
index 0000000..2283fee
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * This page lets the user post a reply to a post.
+ */
+class CreateReplyPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("createReply.html", template, "Page.CreateReply.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val postId = request.httpRequest.getPartAsStringFailsafe("post", 36).apply { templateContext["postId"] = this }
+               val text = request.httpRequest.getPartAsStringFailsafe("text", 65536).trim().apply { templateContext["text"] = this }
+               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256).apply { templateContext["returnPage"] = this }
+               if (request.isPOST) {
+                       if (text == "") {
+                               templateContext["errorTextEmpty"] = true
+                               return
+                       }
+                       val post = webInterface.core.getPost(postId).orNull() ?: throw RedirectException("noPermission.html")
+                       val sender = webInterface.core.getLocalSone(request.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(request.toadletContext)
+                       webInterface.core.createReply(sender, post, TextFilter.filter(request.httpRequest.getHeader("Host"), text))
+                       throw RedirectException(returnPage)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/CreateSonePage.kt
new file mode 100644 (file)
index 0000000..f864b73
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.web.pages
+
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * The “create Sone” page lets the user create a new Sone.
+ */
+class CreateSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("createSone.html", template, "Page.CreateSone.Title", webInterface, false) {
+
+       private val logger = Logger.getLogger(CreateSonePage::class.java.name)
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               templateContext["sones"] = webInterface.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
+               templateContext["identitiesWithoutSone"] = webInterface.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}".toLowerCase() }
+               if (request.isPOST) {
+                       val identity = request.httpRequest.getPartAsStringFailsafe("identity", 43)
+                       webInterface.core.identityManager.allOwnIdentities.firstOrNull { it.id == identity }?.let { ownIdentity ->
+                               val sone = webInterface.core.createSone(ownIdentity)
+                               if (sone == null) {
+                                       logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: $ownIdentity")
+                               }
+                               setCurrentSone(request.toadletContext, sone)
+                               throw RedirectException("index.html")
+                       }
+                       templateContext["errorNoIdentity"] = true
+               }
+       }
+
+       override fun isEnabled(toadletContext: ToadletContext) =
+                       if (webInterface.core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess) {
+                               false
+                       } else {
+                               (getCurrentSone(toadletContext) == null) || (webInterface.core.localSones.size == 1)
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPage.kt
new file mode 100644 (file)
index 0000000..9e6bf38
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user delete an {@link Album}.
+ */
+class DeleteAlbumPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deleteAlbum.html", template, "Page.DeleteAlbum.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val album = webInterface.core.getAlbum(request.httpRequest.getPartAsStringFailsafe("album", 36))
+               templateContext["album"] = album ?: throw RedirectException("invalid.html")
+               if (request.isPOST) {
+                       if (!album.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (request.httpRequest.getPartAsStringFailsafe("abortDelete", 4) == "true") {
+                               throw RedirectException("imageBrowser.html?album=${album.id}")
+                       }
+                       webInterface.core.deleteAlbum(album)
+                       throw RedirectException(if (album.parent.isRoot) "imageBrowser.html?sone=${album.sone.id}" else "imageBrowser.html?album=${album.parent.id}")
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePage.kt
new file mode 100644 (file)
index 0000000..183705a
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user delete an {@link Image}.
+ */
+class DeleteImagePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deleteImage.html", template, "Page.DeleteImage.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val image = webInterface.core.getImage(request.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
+               if (!image.sone.isLocal) {
+                       throw RedirectException("noPermission.html")
+               }
+               if (request.isPOST) {
+                       if (request.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException("imageBrowser.html?image=${image.id}")
+                       }
+                       webInterface.core.deleteImage(image)
+                       throw RedirectException("imageBrowser.html?album=${image.album.id}")
+               }
+               templateContext["image"] = image
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeletePostPage.kt
new file mode 100644 (file)
index 0000000..99ccb3a
--- /dev/null
@@ -0,0 +1,34 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Lets the user delete a post they made.
+ */
+class DeletePostPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deletePost.html", template, "Page.DeletePost.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val post = webInterface.core.getPost(request.httpRequest.getPartAsStringFailsafe("post", 36)).orNull() ?: throw RedirectException("noPermission.html")
+               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               if (request.isPOST) {
+                       if (!post.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (request.httpRequest.isPartSet("confirmDelete")) {
+                               webInterface.core.deletePost(post)
+                               throw RedirectException(returnPage)
+                       } else if (request.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException(returnPage)
+                       }
+               }
+               templateContext["post"] = post
+               templateContext["returnPage"] = returnPage
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPage.kt
new file mode 100644 (file)
index 0000000..e0dc4be
--- /dev/null
@@ -0,0 +1,28 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user confirm the deletion of a profile field.
+ */
+class DeleteProfileFieldPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deleteProfileField.html", template, "Page.DeleteProfileField.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val currentSone = getCurrentSone(request.toadletContext)
+               val field = currentSone.profile.getFieldById(request.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
+               templateContext["field"] = field
+               if (request.isPOST) {
+                       if (request.httpRequest.getPartAsStringFailsafe("confirm", 4) == "true") {
+                               currentSone.profile = currentSone.profile.apply { removeField(field) }
+                       }
+                       throw RedirectException("editProfile.html#profile-fields")
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPage.kt
new file mode 100644 (file)
index 0000000..f9c51a3
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * This page lets the user delete a reply.
+ */
+class DeleteReplyPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deleteReply.html", template, "Page.DeleteReply.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val replyId = request.httpRequest.getPartAsStringFailsafe("reply", 36)
+               templateContext["reply"] = replyId
+               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               templateContext["returnPage"] = returnPage
+               if (request.isPOST) {
+                       val reply = webInterface.core.getPostReply(replyId).orNull() ?: throw RedirectException("noPermission.html")
+                       if (!reply.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (request.httpRequest.isPartSet("confirmDelete")) {
+                               webInterface.core.deleteReply(reply)
+                               throw RedirectException(returnPage)
+                       }
+                       if (request.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException(returnPage)
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePage.kt
new file mode 100644 (file)
index 0000000..299fb80
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Lets the user delete a Sone. Of course the Sone is not really deleted from
+ * Freenet; merely all references to it are removed from the local plugin
+ * installation.
+ */
+class DeleteSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("deleteSone.html", template, "Page.DeleteSone.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               if (request.isPOST) {
+                       if (request.httpRequest.isPartSet("deleteSone")) {
+                               webInterface.core.deleteSone(getCurrentSone(request.toadletContext))
+                       }
+                       throw RedirectException("index.html")
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPage.kt
new file mode 100644 (file)
index 0000000..6e8a3d2
--- /dev/null
@@ -0,0 +1,22 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user dismiss a notification.
+ */
+class DismissNotificationPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("dismissNotification.html", template, "Page.DismissNotification.Title", webInterface) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               val returnPage = request.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               val notificationId = request.httpRequest.getPartAsStringFailsafe("notification", 36)
+               webInterface.getNotification(notificationId).orNull()?.takeIf { it.isDismissable }?.dismiss()
+               throw RedirectException(returnPage)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/DistrustPage.kt
new file mode 100644 (file)
index 0000000..bb70ecc
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user distrust another Sone. This will assign a
+ * configurable (negative) amount of trust to an identity.
+ *
+ * @see net.pterodactylus.sone.core.Core#distrustSone(Sone, Sone)
+ */
+class DistrustPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("distrust.html", template, "Page.Distrust.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               if (request.isPOST) {
+                       val sone = webInterface.core.getSone(request.httpRequest.getPartAsStringFailsafe("sone", 44)).orNull()
+                       sone?.run { webInterface.core.distrustSone(getCurrentSone(request.toadletContext), this) }
+                       throw RedirectException(request.httpRequest.getPartAsStringFailsafe("returnPage", 256))
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPage.kt
new file mode 100644 (file)
index 0000000..1e45744
--- /dev/null
@@ -0,0 +1,44 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that lets the user edit the name and description of an album.
+ */
+class EditAlbumPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               if (request.isPOST) {
+                       val album = webInterface.core.getAlbum(request.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
+                       album.takeUnless { it.sone.isLocal }?.run { throw RedirectException("noPermission.html") }
+                       if (request.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
+                               album.parent?.moveAlbumUp(album)
+                               webInterface.core.touchConfiguration()
+                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
+                       } else if (request.httpRequest.getPartAsStringFailsafe("moveRight", 4) == "true") {
+                               album.parent?.moveAlbumDown(album)
+                               webInterface.core.touchConfiguration()
+                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
+                       } else {
+                               try {
+                                       album.modify()
+                                                       .setTitle(request.httpRequest.getPartAsStringFailsafe("title", 100))
+                                                       .setDescription(request.httpRequest.getPartAsStringFailsafe("description", 1000))
+                                                       .update()
+                               } catch (e: AlbumTitleMustNotBeEmpty) {
+                                       throw RedirectException("emptyAlbumTitle.html")
+                               }
+                               webInterface.core.touchConfiguration()
+                               throw RedirectException("imageBrowser.html?album=${album.id}")
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt
new file mode 100644 (file)
index 0000000..695816f
--- /dev/null
@@ -0,0 +1,39 @@
+package net.pterodactylus.sone.web.pages
+
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * The login page lets the user log in.
+ */
+class LoginPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("login.html", template, "Page.Login.Title", webInterface) {
+
+       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+               if (request.isPOST) {
+                       val soneId = request.httpRequest.getPartAsStringFailsafe("sone-id", 43)
+                       webInterface.core.getLocalSone(soneId)?.let { sone ->
+                               setCurrentSone(request.toadletContext, sone)
+                               val target = if (request.httpRequest.isParameterSet("target")) request.httpRequest.getPartAsStringFailsafe("target", 256) else "index.html"
+                               throw RedirectException(target)
+                       }
+               }
+               templateContext["sones"] = webInterface.core.localSones.sortedWith(Sone.NICE_NAME_COMPARATOR)
+               templateContext["identitiesWithoutSone"] = webInterface.core.identityManager.allOwnIdentities.filterNot { "Sone" in it.contexts }.sortedBy { "${it.nickname}@${it.id}" }
+       }
+
+       override public fun getRedirectTarget(request: FreenetRequest) =
+                       getCurrentSone(request.toadletContext)?.let { "index.html" }
+
+       override fun isEnabled(toadletContext: ToadletContext) = when {
+               webInterface.core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess -> false
+               else -> getCurrentSoneWithoutCreatingSession(toadletContext) == null
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.kt b/src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.kt
deleted file mode 100644 (file)
index 054917d..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
-import net.pterodactylus.sone.test.deepMock
-import net.pterodactylus.sone.test.selfMock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [CreateAlbumPage].
- */
-class CreateAlbumPageTest: WebPageTest() {
-
-       private val page = CreateAlbumPage(template, webInterface)
-
-       override fun getPage() = page
-
-       private val parentAlbum = createAlbum("parent-id")
-       private val newAlbum = createAlbum("album-id")
-
-       @Before
-       fun setupAlbums() {
-               whenever(core.createAlbum(currentSone, parentAlbum)).thenReturn(newAlbum)
-               whenever(currentSone.rootAlbum).thenReturn(parentAlbum)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("createAlbum.html"))
-       }
-
-       @Test
-       fun `get request shows template`() {
-               page.processTemplate(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `missing name results in attribute being set in template context`() {
-               request("", POST)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["nameMissing"], equalTo<Any>(true))
-       }
-
-       private fun createAlbum(albumId: String) = deepMock<Album>().apply {
-               whenever(id).thenReturn(albumId)
-               selfMock<Album.Modifier>().let { modifier ->
-                       whenever(modifier.update()).thenReturn(this@apply)
-                       whenever(this@apply.modify()).thenReturn(modifier)
-               }
-       }
-
-       @Test
-       fun `title and description are set correctly on the album`() {
-               request("", POST)
-               addAlbum("parent-id", parentAlbum)
-               addHttpRequestParameter("name", "new name")
-               addHttpRequestParameter("description", "new description")
-               addHttpRequestParameter("parent", "parent-id")
-               verifyRedirect("imageBrowser.html?album=album-id") {
-                       verify(newAlbum).modify()
-                       verify(newAlbum.modify()).setTitle("new name")
-                       verify(newAlbum.modify()).setDescription("new description")
-                       verify(newAlbum.modify()).update()
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `root album is used if no parent is specified`() {
-               request("", POST)
-               addHttpRequestParameter("name", "new name")
-               addHttpRequestParameter("description", "new description")
-               verifyRedirect("imageBrowser.html?album=album-id")
-       }
-
-       @Test
-       fun `empty album title redirects to error page`() {
-               request("", POST)
-               whenever(newAlbum.modify().update()).thenThrow(AlbumTitleMustNotBeEmpty::class.java)
-               addHttpRequestParameter("name", "new name")
-               addHttpRequestParameter("description", "new description")
-               verifyRedirect("emptyAlbumTitle.html")
-       }
-
-       @Test
-       fun `album description is filtered`() {
-               request("", POST)
-               addHttpRequestParameter("name", "new name")
-               addHttpRequestParameter("description", "new http://localhost:12345/KSK@foo description")
-               addHttpRequestHeader("Host", "localhost:12345")
-               verifyRedirect("imageBrowser.html?album=album-id") {
-                       verify(newAlbum.modify()).setDescription("new KSK@foo description")
-               }
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.kt b/src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.kt
deleted file mode 100644 (file)
index dba49a8..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [CreateReplyPage].
- */
-class CreateReplyPageTest: WebPageTest() {
-
-       private val page = CreateReplyPage(template, webInterface)
-       override fun getPage() = page
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("createReply.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `reply is created correctly`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "new text")
-               val post = mock<Post>().apply { addPost("post-id", this) }
-               verifyRedirect("return.html") {
-                       verify(core).createReply(currentSone, post, "new text")
-               }
-       }
-
-       @Test
-       fun `reply is filtered`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "new http://localhost:12345/KSK@foo text")
-               addHttpRequestHeader("Host", "localhost:12345")
-               val post = mock<Post>().apply { addPost("post-id", this) }
-               verifyRedirect("return.html") {
-                       verify(core).createReply(currentSone, post, "new KSK@foo text")
-               }
-       }
-
-       @Test
-       fun `reply is created with correct sender`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "new text")
-               addHttpRequestParameter("sender", "sender-id")
-               val sender = mock<Sone>().apply { addLocalSone("sender-id", this) }
-               val post = mock<Post>().apply { addPost("post-id", this) }
-               verifyRedirect("return.html") {
-                       verify(core).createReply(sender, post, "new text")
-               }
-       }
-
-       @Test
-       fun `empty text sets parameters in template contexty`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "  ")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["errorTextEmpty"], equalTo<Any>(true))
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-               assertThat(templateContext["postId"], equalTo<Any>("post-id"))
-               assertThat(templateContext["text"], equalTo<Any>(""))
-       }
-
-       @Test
-       fun `user is redirected to no permissions page if post does not exist`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "new text")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `get request stores parameters in template context`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("text", "new text")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-               assertThat(templateContext["postId"], equalTo<Any>("post-id"))
-               assertThat(templateContext["text"], equalTo<Any>("new text"))
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/NewPageTest.java b/src/test/java/net/pterodactylus/sone/web/NewPageTest.java
deleted file mode 100644 (file)
index 223fcb2..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static java.util.Arrays.asList;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.List;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-
-import com.google.common.base.Optional;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link NewPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NewPageTest extends WebPageTest {
-
-       private final NewPage newPage = new NewPage(template, webInterface);
-
-       @Before
-       public void setupNumberOfPostsPerPage() {
-               webInterface.getCore().getPreferences().setPostsPerPage(5);
-       }
-
-       @Test
-       public void postsAreNotDuplicatedWhenTheyComeFromBothNewPostsAndNewRepliesNotifications() throws Exception {
-               // given
-               Post extraPost = mock(Post.class);
-               List<Post> posts = asList(mock(Post.class), mock(Post.class));
-               List<PostReply> postReplies = asList(mock(PostReply.class), mock(PostReply.class));
-               when(postReplies.get(0).getPost()).thenReturn(Optional.of(posts.get(0)));
-               when(postReplies.get(1).getPost()).thenReturn(Optional.of(extraPost));
-               when(webInterface.getNewPosts(currentSone)).thenReturn(posts);
-               when(webInterface.getNewReplies(currentSone)).thenReturn(postReplies);
-
-               // when
-               newPage.processTemplate(freenetRequest, templateContext);
-
-               // then
-               List<Post> renderedPosts = templateContext.get("posts", List.class);
-               assertThat(renderedPosts, containsInAnyOrder(posts.get(0), posts.get(1), extraPost));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/WebPageTest.java b/src/test/java/net/pterodactylus/sone/web/WebPageTest.java
deleted file mode 100644 (file)
index 6f99879..0000000
+++ /dev/null
@@ -1,384 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.test.GuiceKt.supply;
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.Nonnull;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.core.Preferences;
-import net.pterodactylus.sone.core.UpdateChecker;
-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.Sone;
-import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions;
-import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.web.Method;
-import net.pterodactylus.util.web.Response;
-
-import freenet.clients.http.ToadletContext;
-import freenet.l10n.BaseL10n;
-import freenet.support.SimpleReadOnlyArrayBucket;
-import freenet.support.api.Bucket;
-import freenet.support.api.HTTPRequest;
-import freenet.support.api.HTTPUploadedFile;
-import freenet.support.io.NullBucket;
-
-import com.google.common.base.Optional;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimap;
-import com.google.common.eventbus.EventBus;
-import com.google.common.io.ByteStreams;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.rules.ExpectedException;
-import org.mockito.ArgumentMatchers;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Base class for web page tests.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public abstract class WebPageTest {
-
-       @Rule
-       public final ExpectedException expectedException = ExpectedException.none();
-
-       protected final Template template = new Template();
-       protected final WebInterface webInterface = mock(WebInterface.class, RETURNS_DEEP_STUBS);
-       protected final EventBus eventBus = mock(EventBus.class);
-       protected final Core core = webInterface.getCore();
-       protected final BaseL10n l10n = webInterface.getL10n();
-
-       protected final Sone currentSone = mock(Sone.class);
-
-       protected final TemplateContext templateContext = new TemplateContext();
-       protected final HTTPRequest httpRequest = mock(HTTPRequest.class);
-       protected final Multimap<String, String> requestParameters = ArrayListMultimap.create();
-       protected final Map<String, String> requestHeaders = new HashMap<>();
-       private final Map<String, String> uploadedFilesNames = new HashMap<>();
-       private final Map<String, String> uploadedFilesContentTypes = new HashMap<>();
-       private final Map<String, String> uploadedFilesSources = new HashMap<>();
-       protected final FreenetRequest freenetRequest = mock(FreenetRequest.class);
-       private final PipedOutputStream responseOutputStream = new PipedOutputStream();
-       private final PipedInputStream responseInputStream;
-       protected final Response response = new Response(responseOutputStream);
-       protected final ToadletContext toadletContext = mock(ToadletContext.class);
-
-       private final Set<OwnIdentity> ownIdentities = new HashSet<>();
-       private final Map<String, Sone> sones = new HashMap<>();
-       protected final List<Sone> localSones = new ArrayList<>();
-       private final ListMultimap<String, PostReply> postReplies = ArrayListMultimap.create();
-
-       protected final Injector injector = Guice.createInjector(
-                       supply(WebInterface.class).byInstance(webInterface),
-                       supply(Template.class).byInstance(template)
-       );
-
-       protected WebPageTest() {
-               try {
-                       responseInputStream = new PipedInputStream(responseOutputStream);
-               } catch (IOException e) {
-                       throw new RuntimeException(e);
-               }
-       }
-
-       @Before
-       public final void setupFreenetRequest() {
-               when(freenetRequest.getToadletContext()).thenReturn(toadletContext);
-               when(freenetRequest.getHttpRequest()).thenReturn(httpRequest);
-               when(httpRequest.getMultipleParam(anyString())).thenAnswer(new Answer<String[]>() {
-                       @Override
-                       public String[] answer(InvocationOnMock invocation) throws Throwable {
-                               return requestParameters.get(invocation.<String>getArgument(0)).toArray(new String[0]);
-                       }
-               });
-               when(httpRequest.getPartAsStringFailsafe(anyString(), anyInt())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               String parameter = invocation.getArgument(0);
-                               int maxLength = invocation.getArgument(1);
-                               Collection<String> values = requestParameters.get(parameter);
-                               return requestParameters.containsKey(parameter) ? values.iterator().next().substring(0, Math.min(maxLength, values.iterator().next().length())) : "";
-                       }
-               });
-               when(httpRequest.hasParameters()).thenAnswer(new Answer<Boolean>() {
-                       @Override
-                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                               return !requestParameters.isEmpty();
-                       }
-               });
-               when(httpRequest.getParameterNames()).thenAnswer(new Answer<Collection<String>>() {
-                       @Override
-                       public Collection<String> answer(InvocationOnMock invocation) throws Throwable {
-                               return requestParameters.keySet();
-                       }
-               });
-               when(httpRequest.getParam(anyString())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               String parameter = invocation.getArgument(0);
-                               return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : "";
-                       }
-               });
-               when(httpRequest.getParam(anyString(), ArgumentMatchers.<String>any())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               String parameter = invocation.getArgument(0);
-                               return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : invocation.<String>getArgument(1);
-                       }
-               });
-               when(httpRequest.isParameterSet(anyString())).thenAnswer(new Answer<Boolean>() {
-                       @Override
-                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                               return requestParameters.containsKey(invocation.<String>getArgument(0)) &&
-                                               requestParameters.get(invocation.<String>getArgument(0)).iterator().next() != null;
-                       }
-               });
-               when(httpRequest.isPartSet(anyString())).thenAnswer(new Answer<Boolean>() {
-                       @Override
-                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                               return requestParameters.containsKey(invocation.<String>getArgument(0)) &&
-                                               requestParameters.get(invocation.<String>getArgument(0)).iterator().next() != null;
-                       }
-               });
-               when(httpRequest.getParts()).thenAnswer(new Answer<String[]>() {
-                       @Override
-                       public String[] answer(InvocationOnMock invocation) throws Throwable {
-                               return requestParameters.keySet().toArray(new String[requestParameters.size()]);
-                       }
-               });
-               when(httpRequest.getHeader(anyString())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               return requestHeaders.get(invocation.<String>getArgument(0).toLowerCase());
-                       }
-               });
-               when(httpRequest.getUploadedFile(anyString())).thenAnswer(new Answer<HTTPUploadedFile>() {
-                       @Override
-                       public HTTPUploadedFile answer(InvocationOnMock invocation) throws Throwable {
-                               final String name = invocation.getArgument(0);
-                               if (!uploadedFilesSources.containsKey(name)) {
-                                       return null;
-                               }
-                               return new HTTPUploadedFile() {
-                                       @Override
-                                       public String getContentType() {
-                                               return uploadedFilesContentTypes.get(name);
-                                       }
-
-                                       @Override
-                                       public Bucket getData() {
-                                               try (InputStream inputStream = getClass().getResourceAsStream(uploadedFilesSources.get(name))) {
-                                                       byte[] bytes = ByteStreams.toByteArray(inputStream);
-                                                       return new SimpleReadOnlyArrayBucket(bytes, 0, bytes.length);
-                                               } catch (IOException ioe1) {
-                                                       return new NullBucket();
-                                               }
-                                       }
-
-                                       @Override
-                                       public String getFilename() {
-                                               return uploadedFilesNames.get(name);
-                                       }
-                               };
-                       }
-               });
-       }
-
-       @Before
-       public final void setupCore() {
-               UpdateChecker updateChecker = mock(UpdateChecker.class);
-               when(core.getUpdateChecker()).thenReturn(updateChecker);
-               when(core.getPreferences()).thenReturn(new Preferences(eventBus));
-               when(core.getLocalSone(anyString())).thenReturn(null);
-               when(core.getLocalSones()).thenReturn(localSones);
-               when(core.getSone(anyString())).thenReturn(Optional.<Sone>absent());
-               when(core.getSones()).thenAnswer(new Answer<Collection<Sone>>() {
-                       @Override
-                       public Collection<Sone> answer(InvocationOnMock invocation) throws Throwable {
-                               return sones.values();
-                       }
-               });
-               when(core.getSone(anyString())).thenAnswer(new Answer<Optional<Sone>>() {
-                       @Override
-                       public Optional<Sone> answer(InvocationOnMock invocation) throws Throwable {
-                               return Optional.fromNullable(sones.get(invocation.getArgument(0)));
-                       }
-               });
-               when(core.getPost(anyString())).thenReturn(Optional.<Post>absent());
-               when(core.getPostReply(anyString())).thenReturn(Optional.<PostReply>absent());
-               when(core.getReplies(anyString())).thenAnswer(new Answer<List<PostReply>>() {
-                       @Override
-                       public List<PostReply> answer(InvocationOnMock invocation) throws Throwable {
-                               return postReplies.get(invocation.<String>getArgument(0));
-                       }
-               });
-               when(core.getAlbum(anyString())).thenReturn(null);
-               when(core.getImage(anyString())).thenReturn(null);
-               when(core.getImage(anyString(), anyBoolean())).thenReturn(null);
-               when(core.getTemporaryImage(anyString())).thenReturn(null);
-       }
-
-       @Before
-       public void setupL10n() {
-               when(l10n.getString(anyString())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               return invocation.getArgument(0);
-                       }
-               });
-       }
-
-       @Before
-       public final void setupIdentityManager() {
-               when(core.getIdentityManager().getAllOwnIdentities()).thenReturn(ownIdentities);
-       }
-
-       @Before
-       public final void setupWebInterface() {
-               when(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(currentSone);
-               when(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(currentSone);
-               when(webInterface.getNotification(anyString())).thenReturn(Optional.<Notification>absent());
-               when(webInterface.getNotifications(currentSone)).thenReturn(new ArrayList<Notification>());
-       }
-
-       @Before
-       public void setupSone() {
-               when(currentSone.getOptions()).thenReturn(new DefaultSoneOptions());
-       }
-
-       protected SoneTemplatePage getPage() {
-               return null;
-       }
-
-       protected void unsetCurrentSone() {
-               when(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(null);
-               when(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(null);
-       }
-
-       protected void request(String uri, Method method) {
-               try {
-                       when(httpRequest.getPath()).thenReturn(uri);
-                       when(freenetRequest.getUri()).thenReturn(new URI(uri));
-               } catch (URISyntaxException e) {
-                       throw new RuntimeException(e);
-               }
-               when(freenetRequest.getMethod()).thenReturn(method);
-       }
-
-       protected void addHttpRequestHeader(@Nonnull String name, String value) {
-               requestHeaders.put(name.toLowerCase(), value);
-       }
-
-       protected void addHttpRequestParameter(String name, final String value) {
-               requestParameters.put(name, value);
-       }
-
-       protected void addPost(String postId, Post post) {
-               when(core.getPost(postId)).thenReturn(Optional.fromNullable(post));
-       }
-
-       protected void addPostReply(String postReplyId, PostReply postReply) {
-               if (postReply.getPostId() != null) {
-                       postReplies.put(postReply.getPostId(), postReply);
-               }
-               when(core.getPostReply(postReplyId)).thenReturn(Optional.fromNullable(postReply));
-       }
-
-       protected void addSone(String soneId, Sone sone) {
-               sones.put(soneId, sone);
-       }
-
-       protected void addLocalSone(String soneId, Sone sone) {
-               when(core.getLocalSone(eq(soneId))).thenReturn(sone);
-               localSones.add(sone);
-       }
-
-       protected void addOwnIdentity(OwnIdentity ownIdentity) {
-               ownIdentities.add(ownIdentity);
-       }
-
-       protected void addAlbum(String albumId, Album album) {
-               when(core.getAlbum(eq(albumId))).thenReturn(album);
-       }
-
-       protected void addImage(String imageId, Image image) {
-               when(core.getImage(eq(imageId))).thenReturn(image);
-               when(core.getImage(eq(imageId), anyBoolean())).thenReturn(image);
-       }
-
-       protected void addTemporaryImage(String imageId, TemporaryImage temporaryImage) {
-               when(core.getTemporaryImage(eq(imageId))).thenReturn(temporaryImage);
-       }
-
-       protected void addUploadedFile(@Nonnull String name, @Nonnull String filename, @Nonnull String contentType, @Nonnull String resource) {
-               uploadedFilesNames.put(name, filename);
-               uploadedFilesContentTypes.put(name, contentType);
-               uploadedFilesSources.put(name, resource);
-       }
-
-       protected byte[] getResponseBytes() throws IOException {
-               response.getContent().close();
-               try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-                       ByteStreams.copy(responseInputStream, outputStream);
-                       return outputStream.toByteArray();
-               }
-       }
-
-       protected void addNotification(String notificationId, Notification notification) {
-               when(webInterface.getNotification(eq(notificationId))).thenReturn(Optional.of(notification));
-       }
-
-       protected void verifyRedirect(String target) throws RedirectException {
-               expectedException.expect(redirectsTo(target));
-               getPage().handleRequest(freenetRequest, templateContext);
-       }
-
-       protected void verifyRedirect(String target, Runnable verification) throws RedirectException {
-               expectedException.expect(redirectsTo(target));
-               try {
-                       getPage().handleRequest(freenetRequest, templateContext);
-                       fail();
-               } finally {
-                       verification.run();
-               }
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/pages/NewPageTest.java b/src/test/java/net/pterodactylus/sone/web/pages/NewPageTest.java
new file mode 100644 (file)
index 0000000..2811e8b
--- /dev/null
@@ -0,0 +1,51 @@
+package net.pterodactylus.sone.web.pages;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+
+import com.google.common.base.Optional;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link NewPage}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewPageTest extends WebPageTest {
+
+       private final NewPage newPage = new NewPage(template, webInterface);
+
+       @Before
+       public void setupNumberOfPostsPerPage() {
+               webInterface.getCore().getPreferences().setPostsPerPage(5);
+       }
+
+       @Test
+       public void postsAreNotDuplicatedWhenTheyComeFromBothNewPostsAndNewRepliesNotifications() throws Exception {
+               // given
+               Post extraPost = mock(Post.class);
+               List<Post> posts = asList(mock(Post.class), mock(Post.class));
+               List<PostReply> postReplies = asList(mock(PostReply.class), mock(PostReply.class));
+               when(postReplies.get(0).getPost()).thenReturn(Optional.of(posts.get(0)));
+               when(postReplies.get(1).getPost()).thenReturn(Optional.of(extraPost));
+               when(webInterface.getNewPosts(currentSone)).thenReturn(posts);
+               when(webInterface.getNewReplies(currentSone)).thenReturn(postReplies);
+
+               // when
+               newPage.processTemplate(freenetRequest, templateContext);
+
+               // then
+               List<Post> renderedPosts = templateContext.get("posts", List.class);
+               assertThat(renderedPosts, containsInAnyOrder(posts.get(0), posts.get(1), extraPost));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/web/pages/WebPageTest.java b/src/test/java/net/pterodactylus/sone/web/pages/WebPageTest.java
new file mode 100644 (file)
index 0000000..3c6b1d4
--- /dev/null
@@ -0,0 +1,385 @@
+package net.pterodactylus.sone.web.pages;
+
+import static net.pterodactylus.sone.test.GuiceKt.supply;
+import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.core.Preferences;
+import net.pterodactylus.sone.core.UpdateChecker;
+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.Sone;
+import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Response;
+
+import freenet.clients.http.ToadletContext;
+import freenet.l10n.BaseL10n;
+import freenet.support.SimpleReadOnlyArrayBucket;
+import freenet.support.api.Bucket;
+import freenet.support.api.HTTPRequest;
+import freenet.support.api.HTTPUploadedFile;
+import freenet.support.io.NullBucket;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.eventbus.EventBus;
+import com.google.common.io.ByteStreams;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentMatchers;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Base class for web page tests.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class WebPageTest {
+
+       @Rule
+       public final ExpectedException expectedException = ExpectedException.none();
+
+       protected final Template template = new Template();
+       protected final WebInterface webInterface = mock(WebInterface.class, RETURNS_DEEP_STUBS);
+       protected final EventBus eventBus = mock(EventBus.class);
+       protected final Core core = webInterface.getCore();
+       protected final BaseL10n l10n = webInterface.getL10n();
+
+       protected final Sone currentSone = mock(Sone.class);
+
+       protected final TemplateContext templateContext = new TemplateContext();
+       protected final HTTPRequest httpRequest = mock(HTTPRequest.class);
+       protected final Multimap<String, String> requestParameters = ArrayListMultimap.create();
+       protected final Map<String, String> requestHeaders = new HashMap<>();
+       private final Map<String, String> uploadedFilesNames = new HashMap<>();
+       private final Map<String, String> uploadedFilesContentTypes = new HashMap<>();
+       private final Map<String, String> uploadedFilesSources = new HashMap<>();
+       protected final FreenetRequest freenetRequest = mock(FreenetRequest.class);
+       private final PipedOutputStream responseOutputStream = new PipedOutputStream();
+       private final PipedInputStream responseInputStream;
+       protected final Response response = new Response(responseOutputStream);
+       protected final ToadletContext toadletContext = mock(ToadletContext.class);
+
+       private final Set<OwnIdentity> ownIdentities = new HashSet<>();
+       private final Map<String, Sone> sones = new HashMap<>();
+       protected final List<Sone> localSones = new ArrayList<>();
+       private final ListMultimap<String, PostReply> postReplies = ArrayListMultimap.create();
+
+       protected final Injector injector = Guice.createInjector(
+                       supply(WebInterface.class).byInstance(webInterface),
+                       supply(Template.class).byInstance(template)
+       );
+
+       protected WebPageTest() {
+               try {
+                       responseInputStream = new PipedInputStream(responseOutputStream);
+               } catch (IOException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       @Before
+       public final void setupFreenetRequest() {
+               when(freenetRequest.getToadletContext()).thenReturn(toadletContext);
+               when(freenetRequest.getHttpRequest()).thenReturn(httpRequest);
+               when(httpRequest.getMultipleParam(anyString())).thenAnswer(new Answer<String[]>() {
+                       @Override
+                       public String[] answer(InvocationOnMock invocation) throws Throwable {
+                               return requestParameters.get(invocation.<String>getArgument(0)).toArray(new String[0]);
+                       }
+               });
+               when(httpRequest.getPartAsStringFailsafe(anyString(), anyInt())).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) throws Throwable {
+                               String parameter = invocation.getArgument(0);
+                               int maxLength = invocation.getArgument(1);
+                               Collection<String> values = requestParameters.get(parameter);
+                               return requestParameters.containsKey(parameter) ? values.iterator().next().substring(0, Math.min(maxLength, values.iterator().next().length())) : "";
+                       }
+               });
+               when(httpRequest.hasParameters()).thenAnswer(new Answer<Boolean>() {
+                       @Override
+                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                               return !requestParameters.isEmpty();
+                       }
+               });
+               when(httpRequest.getParameterNames()).thenAnswer(new Answer<Collection<String>>() {
+                       @Override
+                       public Collection<String> answer(InvocationOnMock invocation) throws Throwable {
+                               return requestParameters.keySet();
+                       }
+               });
+               when(httpRequest.getParam(anyString())).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) throws Throwable {
+                               String parameter = invocation.getArgument(0);
+                               return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : "";
+                       }
+               });
+               when(httpRequest.getParam(anyString(), ArgumentMatchers.<String>any())).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) throws Throwable {
+                               String parameter = invocation.getArgument(0);
+                               return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : invocation.<String>getArgument(1);
+                       }
+               });
+               when(httpRequest.isParameterSet(anyString())).thenAnswer(new Answer<Boolean>() {
+                       @Override
+                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                               return requestParameters.containsKey(invocation.<String>getArgument(0)) &&
+                                               requestParameters.get(invocation.<String>getArgument(0)).iterator().next() != null;
+                       }
+               });
+               when(httpRequest.isPartSet(anyString())).thenAnswer(new Answer<Boolean>() {
+                       @Override
+                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                               return requestParameters.containsKey(invocation.<String>getArgument(0)) &&
+                                               requestParameters.get(invocation.<String>getArgument(0)).iterator().next() != null;
+                       }
+               });
+               when(httpRequest.getParts()).thenAnswer(new Answer<String[]>() {
+                       @Override
+                       public String[] answer(InvocationOnMock invocation) throws Throwable {
+                               return requestParameters.keySet().toArray(new String[requestParameters.size()]);
+                       }
+               });
+               when(httpRequest.getHeader(anyString())).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) throws Throwable {
+                               return requestHeaders.get(invocation.<String>getArgument(0).toLowerCase());
+                       }
+               });
+               when(httpRequest.getUploadedFile(anyString())).thenAnswer(new Answer<HTTPUploadedFile>() {
+                       @Override
+                       public HTTPUploadedFile answer(InvocationOnMock invocation) throws Throwable {
+                               final String name = invocation.getArgument(0);
+                               if (!uploadedFilesSources.containsKey(name)) {
+                                       return null;
+                               }
+                               return new HTTPUploadedFile() {
+                                       @Override
+                                       public String getContentType() {
+                                               return uploadedFilesContentTypes.get(name);
+                                       }
+
+                                       @Override
+                                       public Bucket getData() {
+                                               try (InputStream inputStream = getClass().getResourceAsStream(uploadedFilesSources.get(name))) {
+                                                       byte[] bytes = ByteStreams.toByteArray(inputStream);
+                                                       return new SimpleReadOnlyArrayBucket(bytes, 0, bytes.length);
+                                               } catch (IOException ioe1) {
+                                                       return new NullBucket();
+                                               }
+                                       }
+
+                                       @Override
+                                       public String getFilename() {
+                                               return uploadedFilesNames.get(name);
+                                       }
+                               };
+                       }
+               });
+       }
+
+       @Before
+       public final void setupCore() {
+               UpdateChecker updateChecker = mock(UpdateChecker.class);
+               when(core.getUpdateChecker()).thenReturn(updateChecker);
+               when(core.getPreferences()).thenReturn(new Preferences(eventBus));
+               when(core.getLocalSone(anyString())).thenReturn(null);
+               when(core.getLocalSones()).thenReturn(localSones);
+               when(core.getSone(anyString())).thenReturn(Optional.<Sone>absent());
+               when(core.getSones()).thenAnswer(new Answer<Collection<Sone>>() {
+                       @Override
+                       public Collection<Sone> answer(InvocationOnMock invocation) throws Throwable {
+                               return sones.values();
+                       }
+               });
+               when(core.getSone(anyString())).thenAnswer(new Answer<Optional<Sone>>() {
+                       @Override
+                       public Optional<Sone> answer(InvocationOnMock invocation) throws Throwable {
+                               return Optional.fromNullable(sones.get(invocation.getArgument(0)));
+                       }
+               });
+               when(core.getPost(anyString())).thenReturn(Optional.<Post>absent());
+               when(core.getPostReply(anyString())).thenReturn(Optional.<PostReply>absent());
+               when(core.getReplies(anyString())).thenAnswer(new Answer<List<PostReply>>() {
+                       @Override
+                       public List<PostReply> answer(InvocationOnMock invocation) throws Throwable {
+                               return postReplies.get(invocation.<String>getArgument(0));
+                       }
+               });
+               when(core.getAlbum(anyString())).thenReturn(null);
+               when(core.getImage(anyString())).thenReturn(null);
+               when(core.getImage(anyString(), anyBoolean())).thenReturn(null);
+               when(core.getTemporaryImage(anyString())).thenReturn(null);
+       }
+
+       @Before
+       public void setupL10n() {
+               when(l10n.getString(anyString())).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) throws Throwable {
+                               return invocation.getArgument(0);
+                       }
+               });
+       }
+
+       @Before
+       public final void setupIdentityManager() {
+               when(core.getIdentityManager().getAllOwnIdentities()).thenReturn(ownIdentities);
+       }
+
+       @Before
+       public final void setupWebInterface() {
+               when(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(currentSone);
+               when(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(currentSone);
+               when(webInterface.getNotification(anyString())).thenReturn(Optional.<Notification>absent());
+               when(webInterface.getNotifications(currentSone)).thenReturn(new ArrayList<Notification>());
+       }
+
+       @Before
+       public void setupSone() {
+               when(currentSone.getOptions()).thenReturn(new DefaultSoneOptions());
+       }
+
+       protected SoneTemplatePage getPage() {
+               return null;
+       }
+
+       protected void unsetCurrentSone() {
+               when(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(null);
+               when(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(null);
+       }
+
+       protected void request(String uri, Method method) {
+               try {
+                       when(httpRequest.getPath()).thenReturn(uri);
+                       when(freenetRequest.getUri()).thenReturn(new URI(uri));
+               } catch (URISyntaxException e) {
+                       throw new RuntimeException(e);
+               }
+               when(freenetRequest.getMethod()).thenReturn(method);
+       }
+
+       protected void addHttpRequestHeader(@Nonnull String name, String value) {
+               requestHeaders.put(name.toLowerCase(), value);
+       }
+
+       protected void addHttpRequestParameter(String name, final String value) {
+               requestParameters.put(name, value);
+       }
+
+       protected void addPost(String postId, Post post) {
+               when(core.getPost(postId)).thenReturn(Optional.fromNullable(post));
+       }
+
+       protected void addPostReply(String postReplyId, PostReply postReply) {
+               if (postReply.getPostId() != null) {
+                       postReplies.put(postReply.getPostId(), postReply);
+               }
+               when(core.getPostReply(postReplyId)).thenReturn(Optional.fromNullable(postReply));
+       }
+
+       protected void addSone(String soneId, Sone sone) {
+               sones.put(soneId, sone);
+       }
+
+       protected void addLocalSone(String soneId, Sone sone) {
+               when(core.getLocalSone(eq(soneId))).thenReturn(sone);
+               localSones.add(sone);
+       }
+
+       protected void addOwnIdentity(OwnIdentity ownIdentity) {
+               ownIdentities.add(ownIdentity);
+       }
+
+       protected void addAlbum(String albumId, Album album) {
+               when(core.getAlbum(eq(albumId))).thenReturn(album);
+       }
+
+       protected void addImage(String imageId, Image image) {
+               when(core.getImage(eq(imageId))).thenReturn(image);
+               when(core.getImage(eq(imageId), anyBoolean())).thenReturn(image);
+       }
+
+       protected void addTemporaryImage(String imageId, TemporaryImage temporaryImage) {
+               when(core.getTemporaryImage(eq(imageId))).thenReturn(temporaryImage);
+       }
+
+       protected void addUploadedFile(@Nonnull String name, @Nonnull String filename, @Nonnull String contentType, @Nonnull String resource) {
+               uploadedFilesNames.put(name, filename);
+               uploadedFilesContentTypes.put(name, contentType);
+               uploadedFilesSources.put(name, resource);
+       }
+
+       protected byte[] getResponseBytes() throws IOException {
+               response.getContent().close();
+               try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+                       ByteStreams.copy(responseInputStream, outputStream);
+                       return outputStream.toByteArray();
+               }
+       }
+
+       protected void addNotification(String notificationId, Notification notification) {
+               when(webInterface.getNotification(eq(notificationId))).thenReturn(Optional.of(notification));
+       }
+
+       protected void verifyRedirect(String target) throws RedirectException {
+               expectedException.expect(redirectsTo(target));
+               getPage().handleRequest(freenetRequest, templateContext);
+       }
+
+       protected void verifyRedirect(String target, Runnable verification) throws RedirectException {
+               expectedException.expect(redirectsTo(target));
+               try {
+                       getPage().handleRequest(freenetRequest, templateContext);
+                       fail();
+               } finally {
+                       verification.run();
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/AboutPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/AboutPageTest.kt
deleted file mode 100644 (file)
index d4adf3a..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.main.SonePlugin.PluginHomepage
-import net.pterodactylus.sone.main.SonePlugin.PluginVersion
-import net.pterodactylus.sone.main.SonePlugin.PluginYear
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-
-/**
- * Unit test for [AboutPage].
- */
-class AboutPageTest: WebPageTest() {
-
-       private val version = "0.1.2"
-       private val year = 1234
-       private val homepage = "home://page"
-       private val page = AboutPage(template, webInterface, PluginVersion(version), PluginYear(year), PluginHomepage(homepage))
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("about.html"))
-       }
-
-       @Test
-       fun `page does not require login`() {
-               assertThat(page.requiresLogin(), equalTo(false))
-       }
-
-       @Test
-       fun `page sets correct version in template context`() {
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["version"], equalTo<Any>(version))
-       }
-
-       @Test
-       fun `page sets correct homepage in template context`() {
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["homepage"], equalTo<Any>(homepage))
-       }
-
-       @Test
-       fun `page sets correct year in template context`() {
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["year"], equalTo<Any>(year))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/BookmarkPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/BookmarkPageTest.kt
deleted file mode 100644 (file)
index bebe10c..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [BookmarkPage].
- */
-class BookmarkPageTest : WebPageTest() {
-
-       private val page = BookmarkPage(template, webInterface)
-       override fun getPage() = page
-
-       @Test
-       fun `path is set correctly`() {
-               assertThat(page.path, equalTo("bookmark.html"))
-       }
-
-       @Test
-       fun `get request does not bookmark anything and does not redirect`() {
-               page.processTemplate(freenetRequest, templateContext)
-               verify(core, never()).bookmarkPost(any())
-       }
-
-       private fun setupBookmarkRequest() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return-page.html")
-               addHttpRequestParameter("post", "post-id")
-       }
-
-       @Test
-       fun `post is bookmarked correctly`() {
-               setupBookmarkRequest()
-               val post = mock<Post>()
-               addPost("post-id", post)
-               verifyRedirect("return-page.html") {
-                       verify(core).bookmarkPost(post)
-               }
-       }
-
-       @Test
-       fun `non-existing post is not bookmarked`() {
-               setupBookmarkRequest()
-               verifyRedirect("return-page.html") {
-                       verify(core, never()).bookmarkPost(any())
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/BookmarksPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/BookmarksPageTest.kt
deleted file mode 100644 (file)
index cfde866..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.collection.Pagination
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-
-/**
- * Unit test for [BookmarksPage].
- */
-class BookmarksPageTest: WebPageTest() {
-
-       private val page = BookmarksPage(template, webInterface)
-       private val post1 = createLoadedPost(1000)
-       private val post2 = createLoadedPost(3000)
-       private val post3 = createLoadedPost(2000)
-
-       private fun createLoadedPost(time: Long) = mock<Post>().apply {
-               whenever(isLoaded).thenReturn(true)
-               whenever(this.time).thenReturn(time)
-       }
-
-       @Before
-       fun setupBookmarkedPostsAndPagination() {
-               whenever(core.bookmarkedPosts).thenReturn(setOf(post1, post2, post3))
-               core.preferences.postsPerPage = 5
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("bookmarks.html"))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `page sets correct posts in template context`() {
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["posts"] as Collection<Post>, contains(post2, post3, post1))
-               assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(post2, post3, post1))
-               assertThat(templateContext["postsNotLoaded"], equalTo<Any>(false))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `page does not put unloaded posts in template context but sets a flag`() {
-               whenever(post3.isLoaded).thenReturn(false)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["posts"] as Collection<Post>, contains(post2, post1))
-               assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(post2, post1))
-               assertThat(templateContext["postsNotLoaded"], equalTo<Any>(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/CreatePostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/CreatePostPageTest.kt
deleted file mode 100644 (file)
index f1a120e..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-package net.pterodactylus.sone.web
-
-import com.google.common.base.Optional.absent
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.asOptional
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [CreatePostPage].
- */
-class CreatePostPageTest: WebPageTest() {
-
-       private val page = CreatePostPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("createPost.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `return page is set in template context`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-       }
-
-       @Test
-       fun `post is created correctly`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("text", "post text")
-               verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, absent(), "post text")
-               }
-       }
-
-       @Test
-       fun `creating an empty post is denied`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("text", "  ")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["errorTextEmpty"], equalTo<Any>(true))
-       }
-
-       @Test
-       fun `a sender can be selected`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("text", "post text")
-               addHttpRequestParameter("sender", "sender-id")
-               val sender = mock<Sone>()
-               addLocalSone("sender-id", sender)
-               verifyRedirect("return.html") {
-                       verify(core).createPost(sender, absent(), "post text")
-               }
-       }
-
-       @Test
-       fun `a recipient can be selected`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("text", "post text")
-               addHttpRequestParameter("recipient", "recipient-id")
-               val recipient = mock<Sone>()
-               addSone("recipient-id", recipient)
-               verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, recipient.asOptional(), "post text")
-               }
-       }
-
-       @Test
-       fun `text is filtered correctly`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("text", "post http://localhost:12345/KSK@foo text")
-               addHttpRequestHeader("Host", "localhost:12345")
-               verifyRedirect("return.html") {
-                       verify(core).createPost(currentSone, absent(), "post KSK@foo text")
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/CreateSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/CreateSonePageTest.kt
deleted file mode 100644 (file)
index 80239e9..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.freenet.wot.OwnIdentity
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [CreateSonePage].
- */
-class CreateSonePageTest: WebPageTest() {
-
-       private val page = CreateSonePage(template, webInterface)
-       override fun getPage() = page
-
-       private val localSones_ = listOf(
-                       createSone("local-sone1"),
-                       createSone("local-sone2"),
-                       createSone("local-sone3")
-       )
-
-       private fun createSone(id: String) = mock<Sone>().apply {
-               whenever(this.id).thenReturn(id)
-               whenever(profile).thenReturn(Profile(this))
-       }
-
-       private val ownIdentities_ = listOf(
-                       createOwnIdentity("own-id-1", "Sone"),
-                       createOwnIdentity("own-id-2", "Test", "Foo"),
-                       createOwnIdentity("own-id-3"),
-                       createOwnIdentity("own-id-4", "Sone")
-       )
-
-       private fun createOwnIdentity(id: String, vararg contexts: String) = mock<OwnIdentity>().apply {
-               whenever(this.id).thenReturn(id)
-               whenever(this.nickname).thenReturn(id)
-               whenever(this.contexts).thenReturn(contexts.toSet())
-               whenever(this.hasContext(anyString())).thenAnswer { invocation -> invocation.getArgument<String>(0) in contexts }
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("createSone.html"))
-       }
-
-       @Test
-       fun `page does not require login`() {
-               assertThat(page.requiresLogin(), equalTo(false))
-       }
-
-       private fun addExistingSones() {
-               listOf(2, 0, 1).map { localSones_[it] }.forEach { addLocalSone(it.id, it) }
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `get request stores sorted list of local sones in template context`() {
-               addExistingSones()
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["sones"] as Collection<Sone>, contains(localSones_[0], localSones_[1], localSones_[2]))
-       }
-
-       private fun addExistingOwnIdentities() {
-               listOf(2, 0, 3, 1).map { ownIdentities_[it] }.forEach { addOwnIdentity(it) }
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `get request stores sorted sones without sone context in the template context`() {
-               addExistingOwnIdentities()
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["identitiesWithoutSone"] as Collection<OwnIdentity>, contains(ownIdentities_[1], ownIdentities_[2]))
-       }
-
-       @Test
-       fun `sone is created and logged in`() {
-               addExistingOwnIdentities()
-               request("", POST)
-               addHttpRequestParameter("identity", "own-id-3")
-               val newSone = mock<Sone>()
-               whenever(core.createSone(ownIdentities_[2])).thenReturn(newSone)
-               verifyRedirect("index.html") {
-                       verify(webInterface).setCurrentSone(toadletContext, newSone)
-               }
-       }
-
-       @Test
-       fun `on invalid identity id a flag is set in the template context`() {
-               request("", POST)
-               addHttpRequestParameter("identity", "own-id-3")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["errorNoIdentity"], equalTo<Any>(true))
-       }
-
-       @Test
-       fun `if sone is not created user is still redirected to index`() {
-               addExistingOwnIdentities()
-               request("", POST)
-               addHttpRequestParameter("identity", "own-id-3")
-               whenever(core.createSone(ownIdentities_[2])).thenReturn(null)
-               verifyRedirect("index.html") {
-                       verify(core).createSone(ownIdentities_[2])
-                       verify(webInterface).setCurrentSone(toadletContext, null)
-               }
-       }
-
-       @Test
-       fun `create sone is not shown in menu if full access is required but client doesn’t have full access`() {
-               core.preferences.isRequireFullAccess = true
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `create sone is shown in menu if no sone is logged in`() {
-               unsetCurrentSone()
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `create sone is shown in menu if a single sone exists`() {
-               addLocalSone("local-sone", localSones_[0])
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `create sone is not shown in menu if more than one sone exists`() {
-               addLocalSone("local-sone1", localSones_[0])
-               addLocalSone("local-sone2", localSones_[1])
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `create sone is shown in menu if no sone is logged in and client has full access`() {
-               core.preferences.isRequireFullAccess = true
-               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
-               unsetCurrentSone()
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeleteAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeleteAlbumPageTest.kt
deleted file mode 100644 (file)
index 9276be7..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeleteAlbumPage].
- */
-class DeleteAlbumPageTest: WebPageTest() {
-
-       private val page = DeleteAlbumPage(template, webInterface)
-
-       private val sone = mock<Sone>()
-       private val album = mock<Album>()
-       private val parentAlbum = mock<Album>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupAlbums() {
-               whenever(sone.id).thenReturn("sone-id")
-               whenever(sone.isLocal).thenReturn(true)
-               whenever(parentAlbum.id).thenReturn("parent-id")
-               whenever(parentAlbum.isRoot).thenReturn(true)
-               whenever(album.id).thenReturn("album-id")
-               whenever(album.sone).thenReturn(sone)
-               whenever(album.parent).thenReturn(parentAlbum)
-               whenever(sone.rootAlbum).thenReturn(parentAlbum)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("deleteAlbum.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `get request with invalid album ID results in redirect to invalid page`() {
-               request("", GET)
-               whenever(core.getAlbum(anyString())).thenReturn(null)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `get request with valid album ID sets album in template context`() {
-               request("", GET)
-               val album = mock<Album>()
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["album"], equalTo<Any>(album))
-       }
-
-       @Test
-       fun `post request redirects to invalid page if album is invalid`() {
-               request("", POST)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `post request redirects to no permissions page if album is not local`() {
-               request("", POST)
-               whenever(sone.isLocal).thenReturn(false)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with abort delete parameter set redirects to album browser`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               addHttpRequestParameter("abortDelete", "true")
-               verifyRedirect("imageBrowser.html?album=album-id")
-       }
-
-       @Test
-       fun `album is deleted and page redirects to sone if parent album is root album`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               verifyRedirect("imageBrowser.html?sone=sone-id") {
-                       verify(core).deleteAlbum(album)
-               }
-       }
-
-       @Test
-       fun `album is deleted and page redirects to album if parent album is not root album`() {
-               request("", POST)
-               whenever(parentAlbum.isRoot).thenReturn(false)
-               whenever(sone.rootAlbum).thenReturn(mock<Album>())
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(core).deleteAlbum(album)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeleteImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeleteImagePageTest.kt
deleted file mode 100644 (file)
index 3f617cd..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeleteImagePage].
- */
-class DeleteImagePageTest: WebPageTest() {
-
-       private val page = DeleteImagePage(template, webInterface)
-       private val image = mock<Image>()
-       private val sone = mock<Sone>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupImage() {
-               val album = mock<Album>()
-               whenever(album.id).thenReturn("album-id")
-               whenever(image.id).thenReturn("image-id")
-               whenever(image.sone).thenReturn(sone)
-               whenever(image.album).thenReturn(album)
-               whenever(sone.isLocal).thenReturn(true)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("deleteImage.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `get request with invalid image redirects to invalid page`() {
-               request("", GET)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `get request with image from non-local sone redirects to no permissions page`() {
-               request("", GET)
-               whenever(sone.isLocal).thenReturn(false)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `get request with image from local sone sets image in template context`() {
-               request("", GET)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["image"], equalTo<Any>(image))
-       }
-
-       @Test
-       fun `post request with abort delete flag set redirects to image browser`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("abortDelete", "true")
-               verifyRedirect("imageBrowser.html?image=image-id")
-       }
-
-       @Test
-       fun `post request deletes image and redirects to image browser`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               verifyRedirect("imageBrowser.html?album=album-id") {
-                       verify(webInterface.core).deleteImage(image)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeletePostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeletePostPageTest.kt
deleted file mode 100644 (file)
index ab7851a..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeletePostPage].
- */
-class DeletePostPageTest : WebPageTest() {
-
-       private val page = DeletePostPage(template, webInterface)
-
-       private val post = mock<Post>()
-       private val sone = mock<Sone>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupPost() {
-               whenever(post.sone).thenReturn(sone)
-               whenever(sone.isLocal).thenReturn(true)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-           assertThat(page.path, equalTo("deletePost.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-           assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `get request with invalid post redirects to no permission page`() {
-               request("", GET)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `get request with valid post sets post and return page in template context`() {
-               request("", GET)
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["post"], equalTo<Any>(post))
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-       }
-
-       @Test
-       fun `post request with invalid post redirects to no permission page`() {
-               request("", POST)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with post from non-local sone redirects to no permission page`() {
-               request("", POST)
-               whenever(sone.isLocal).thenReturn(false)
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with confirmation deletes post and redirects to return page`() {
-               request("", POST)
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("confirmDelete", "true")
-               verifyRedirect("return.html") {
-                       verify(core).deletePost(post)
-               }
-       }
-
-       @Test
-       fun `post request with abort delete does not delete post and redirects to return page`() {
-               request("", POST)
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("abortDelete", "true")
-               verifyRedirect("return.html") {
-                       verify(core, never()).deletePost(post)
-               }
-       }
-
-       @Test
-       fun `post request without delete or abort sets post in template context`() {
-               request("", POST)
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["post"], equalTo<Any>(post))
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeleteProfileFieldPageTest.kt
deleted file mode 100644 (file)
index 4c5a07a..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeleteProfileFieldPage].
- */
-class DeleteProfileFieldPageTest: WebPageTest() {
-
-       private val page = DeleteProfileFieldPage(template, webInterface)
-
-       private val profile = Profile(currentSone)
-       private val field = profile.addField("name")
-
-       override fun getPage() = page
-
-       @Before
-       fun setupProfile() {
-               whenever(currentSone.profile).thenReturn(profile)
-               field.value = "value"
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("deleteProfileField.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `get request with invalid field name redirects to invalid page`() {
-               request("", GET)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `post request with invalid field name redirects to invalid page`() {
-               request("", POST)
-               addHttpRequestParameter("field", "wrong-id")
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `get request with valid field name sets field in template context`() {
-               request("", GET)
-               addHttpRequestParameter("field", field.id)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["field"], equalTo<Any>(field))
-       }
-
-       @Test
-       fun `post request without confirm redirects to edit profile page`() {
-               request("", POST)
-               addHttpRequestParameter("field", field.id)
-               verifyRedirect("editProfile.html#profile-fields") {
-                       verify(currentSone, never()).profile = any()
-               }
-       }
-
-       @Test
-       fun `post request with confirm removes field and redirects to edit profile page`() {
-               request("", POST)
-               addHttpRequestParameter("field", field.id)
-               addHttpRequestParameter("confirm", "true")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(profile.getFieldById(field.id), nullValue())
-                       verify(currentSone).profile = profile
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeleteReplyPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeleteReplyPageTest.kt
deleted file mode 100644 (file)
index b7b92ae..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.PostReply
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeleteReplyPage].
- */
-class DeleteReplyPageTest : WebPageTest() {
-
-       private val page = DeleteReplyPage(template, webInterface)
-
-       private val sone = mock<Sone>()
-       private val reply = mock<PostReply>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupReply() {
-               whenever(sone.isLocal).thenReturn(true)
-               whenever(reply.sone).thenReturn(sone)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-           assertThat(page.path, equalTo("deleteReply.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-           assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `get request sets reply ID and return page in template context`() {
-               request("", GET)
-               addHttpRequestParameter("reply", "reply-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["reply"], equalTo<Any>("reply-id"))
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-       }
-
-       @Test
-       fun `post request without any action sets reply ID and return page in template context`() {
-               request("", POST)
-               addPostReply("reply-id", reply)
-               addHttpRequestParameter("reply", "reply-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["reply"], equalTo<Any>("reply-id"))
-               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
-       }
-
-       @Test
-       fun `trying to delete a reply with an invalid ID results in no permission page`() {
-               request("", POST)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `trying to delete a reply from a non-local sone results in no permission page`() {
-               request("", POST)
-               addHttpRequestParameter("reply", "reply-id")
-               whenever(sone.isLocal).thenReturn(false)
-               addPostReply("reply-id", reply)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `confirming deletion of reply deletes the reply and redirects to return page`() {
-               request("", POST)
-               addPostReply("reply-id", reply)
-               addHttpRequestParameter("reply", "reply-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("confirmDelete", "true")
-               verifyRedirect("return.html") {
-                       verify(core).deleteReply(reply)
-               }
-       }
-
-       @Test
-       fun `aborting deletion of reply redirects to return page`() {
-               request("", POST)
-               addPostReply("reply-id", reply)
-               addHttpRequestParameter("reply", "reply-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("abortDelete", "true")
-               verifyRedirect("return.html") {
-                       verify(core, never()).deleteReply(reply)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DeleteSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DeleteSonePageTest.kt
deleted file mode 100644 (file)
index b51ebed..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.ArgumentMatchers
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DeleteSonePage].
- */
-class DeleteSonePageTest : WebPageTest() {
-
-       private val page = DeleteSonePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `page returns correct path`() {
-           assertThat(page.path, equalTo("deleteSone.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-           assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-           whenever(l10n.getString("Page.DeleteSone.Title")).thenReturn("delete sone page")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("delete sone page"))
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.processTemplate(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request without delete confirmation redirects to index`() {
-               request("", POST)
-               verifyRedirect("index.html") {
-                       verify(core, never()).deleteSone(any())
-               }
-       }
-
-       @Test
-       fun `post request with delete confirmation deletes sone and redirects to index`() {
-               request("", POST)
-               addHttpRequestParameter("deleteSone", "true")
-               verifyRedirect("index.html") {
-                       verify(core).deleteSone(currentSone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DismissNotificationPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DismissNotificationPageTest.kt
deleted file mode 100644 (file)
index 3c19f5c..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.notify.Notification
-import net.pterodactylus.util.web.Method.GET
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DismissNotificationPage].
- */
-class DismissNotificationPageTest: WebPageTest() {
-
-       private val page = DismissNotificationPage(template, webInterface)
-       private val notification = mock<Notification>()
-
-       override fun getPage() = page
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("dismissNotification.html"))
-       }
-
-       @Test
-       fun `page does not require login`() {
-               assertThat(page.requiresLogin(), equalTo(false))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               whenever(l10n.getString("Page.DismissNotification.Title")).thenReturn("dismiss notification page")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("dismiss notification page"))
-       }
-
-       @Test
-       fun `get request with invalid notification ID redirects to return page`() {
-               request("", GET)
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html")
-       }
-
-       @Test
-       fun `get request with non-dismissible notification never dismisses the notification but redirects to return page`() {
-               request("", GET)
-               addNotification("notification-id", notification)
-               addHttpRequestParameter("notification", "notification-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(notification, never()).dismiss()
-               }
-       }
-
-       @Test
-       fun `get request with dismissible notification dismisses the notification and redirects to return page`() {
-               request("", GET)
-               whenever(notification.isDismissable).thenReturn(true)
-               addNotification("notification-id", notification)
-               addHttpRequestParameter("notification", "notification-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(notification).dismiss()
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/DistrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/DistrustPageTest.kt
deleted file mode 100644 (file)
index 8346a7f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [DistrustPage].
- */
-class DistrustPageTest: WebPageTest() {
-
-       private val page = DistrustPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("distrust.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               whenever(l10n.getString("Page.Distrust.Title")).thenReturn("distrust page title")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("distrust page title"))
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.processTemplate(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with invalid sone redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html")
-       }
-
-       @Test
-       fun `post request with valid sone distrusts sone and redirects to return page`() {
-               request("", POST)
-               val remoteSone = mock<Sone>()
-               addSone("remote-sone-id", remoteSone)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "remote-sone-id")
-               verifyRedirect("return.html") {
-                       verify(core).distrustSone(currentSone, remoteSone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/EditAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/EditAlbumPageTest.kt
deleted file mode 100644 (file)
index ed96a80..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.mockBuilder
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [EditAlbumPage].
- */
-class EditAlbumPageTest: WebPageTest() {
-
-       private val page = EditAlbumPage(template, webInterface)
-
-       private val album = mock<Album>()
-       private val parentAlbum = mock<Album>()
-       private val modifier = mockBuilder<Album.Modifier>()
-       private val sone = mock<Sone>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setup() {
-               whenever(album.id).thenReturn("album-id")
-               whenever(album.sone).thenReturn(sone)
-               whenever(album.parent).thenReturn(parentAlbum)
-               whenever(album.modify()).thenReturn(modifier)
-               whenever(modifier.update()).thenReturn(album)
-               whenever(parentAlbum.id).thenReturn("parent-id")
-               whenever(sone.isLocal).thenReturn(true)
-               addHttpRequestHeader("Host", "www.te.st")
-       }
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("editAlbum.html"))
-       }
-
-       @Test
-       fun `page requires login`() {
-               assertThat(page.requiresLogin(), equalTo(true))
-       }
-
-       @Test
-       fun `page returns correct title`() {
-               whenever(l10n.getString("Page.EditAlbum.Title")).thenReturn("edit album page")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("edit album page"))
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.processTemplate(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with invalid album redirects to invalid page`() {
-               request("", POST)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `post request with album of non-local sone redirects to no permissions page`() {
-               request("", POST)
-               whenever(sone.isLocal).thenReturn(false)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with move left requested moves album to the left and redirects to album browser`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               addHttpRequestParameter("moveLeft", "true")
-               verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(parentAlbum).moveAlbumUp(album)
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with move right requested moves album to the left and redirects to album browser`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               addHttpRequestParameter("moveRight", "true")
-               verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(parentAlbum).moveAlbumDown(album)
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with empty album title redirects to empty album title page`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               whenever(modifier.setTitle("")).thenThrow(AlbumTitleMustNotBeEmpty())
-               verifyRedirect("emptyAlbumTitle.html")
-       }
-
-       @Test
-       fun `post request with non-empty album title and description redirects to album browser`() {
-               request("", POST)
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               addHttpRequestParameter("title", "title")
-               addHttpRequestParameter("description", "description")
-               verifyRedirect("imageBrowser.html?album=album-id") {
-                       verify(modifier).setTitle("title")
-                       verify(modifier).setDescription("description")
-                       verify(modifier).update()
-                       verify(core).touchConfiguration()
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/EditImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/EditImagePageTest.kt
deleted file mode 100644 (file)
index 04ce570..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.mockBuilder
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [EditImagePage].
- */
-class EditImagePageTest : WebPageTest() {
-
-       private val page = EditImagePage(template, webInterface)
-
-       private val image = mock<Image>()
-       private val modifier = mockBuilder<Image.Modifier>()
-       private val sone = mock<Sone>()
-       private val album = mock<Album>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupImage() {
-               whenever(sone.isLocal).thenReturn(true)
-               whenever(album.id).thenReturn("album-id")
-               whenever(modifier.update()).thenReturn(image)
-               whenever(image.sone).thenReturn(sone)
-               whenever(image.album).thenReturn(album)
-               whenever(image.modify()).thenReturn(modifier)
-       }
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with invalid image redirects to invalid page`() {
-               request("", POST)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `post request with valid image from non-local sone redirects to no permission page`() {
-               request("", POST)
-               whenever(sone.isLocal).thenReturn(false)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with valid image and move left requested moves image left and redirects to return page`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("moveLeft", "true")
-               verifyRedirect("return.html") {
-                       verify(album).moveImageUp(image)
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with valid image and move right requested moves image right and redirects to return page`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("moveRight", "true")
-               verifyRedirect("return.html") {
-                       verify(album).moveImageDown(image)
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with valid image but only whitespace in the title redirects to empty image title page`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("title", "   ")
-               verifyRedirect("emptyImageTitle.html") {
-                       verify(core, never()).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with valid image title and description modifies image and redirects to reutrn page`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("title", "Title")
-               addHttpRequestParameter("description", "Description")
-               verifyRedirect("return.html") {
-                       verify(modifier).setTitle("Title")
-                       verify(modifier).setDescription("Description")
-                       verify(modifier).update()
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `post request with image title and description modifies image with filtered description and redirects to reutrn page`() {
-               request("", POST)
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("title", "Title")
-               addHttpRequestHeader("Host", "www.te.st")
-               addHttpRequestParameter("description", "Get http://www.te.st/KSK@GPL.txt")
-               verifyRedirect("return.html") {
-                       verify(modifier).setTitle("Title")
-                       verify(modifier).setDescription("Get KSK@GPL.txt")
-                       verify(modifier).update()
-                       verify(core).touchConfiguration()
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/EditProfileFieldPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/EditProfileFieldPageTest.kt
deleted file mode 100644 (file)
index 01d78ca..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [EditProfileFieldPage].
- */
-class EditProfileFieldPageTest : WebPageTest() {
-
-       private val page = EditProfileFieldPage(template, webInterface)
-
-       private val profile = Profile(currentSone)
-       private val field = profile.addField("Name")
-
-       override fun getPage() = page
-
-       @Before
-       fun setupProfile() {
-               whenever(currentSone.profile).thenReturn(profile)
-       }
-
-       @Test
-       fun `get request with invalid field redirects to invalid page`() {
-               request("", GET)
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `get request with valid field stores field in template context`() {
-               request("", GET)
-               addHttpRequestParameter("field", field.id)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["field"], equalTo<Any>(field))
-       }
-
-       @Test
-       fun `post request with cancel set redirects to profile edit page`() {
-               request("", POST)
-               addHttpRequestParameter("field", field.id)
-               addHttpRequestParameter("cancel", "true")
-               verifyRedirect("editProfile.html#profile-fields")
-       }
-
-       @Test
-       fun `post request with new name renames field and redirects to profile edit page`() {
-               request("", POST)
-               addHttpRequestParameter("field", field.id)
-               addHttpRequestParameter("name", "New Name")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(field.name, equalTo("New Name"))
-                       verify(currentSone).profile = profile
-               }
-       }
-
-       @Test
-       fun `post request with same name does not modify field and redirects to profile edit page`() {
-               request("", POST)
-               addHttpRequestParameter("field", field.id)
-               addHttpRequestParameter("name", "Name")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(field.name, equalTo("Name"))
-                       verify(currentSone, never()).profile = profile
-               }
-       }
-
-       @Test
-       fun `post request with same name as different field sets error condition in template`() {
-               request("", POST)
-               profile.addField("New Name")
-               addHttpRequestParameter("field", field.id)
-               addHttpRequestParameter("name", "New Name")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(field.name, equalTo("Name"))
-               verify(currentSone, never()).profile = profile
-               assertThat(templateContext["duplicateFieldName"], equalTo<Any>(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/EditProfilePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/EditProfilePageTest.kt
deleted file mode 100644 (file)
index 0fb6cca..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.notNullValue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [EditProfilePage].
- */
-class EditProfilePageTest : WebPageTest() {
-
-       private val page = EditProfilePage(template, webInterface)
-
-       private val profile = Profile(currentSone)
-       private val firstField = profile.addField("First Field")
-       private val secondField = profile.addField("Second Field")
-
-       override fun getPage() = page
-
-       @Before
-       fun setupProfile() {
-               val avatar = mock<Image>()
-               whenever(avatar.id).thenReturn("image-id")
-               whenever(avatar.sone).thenReturn(currentSone)
-               profile.firstName = "First"
-               profile.middleName = "Middle"
-               profile.lastName = "Last"
-               profile.birthDay = 31
-               profile.birthMonth = 12
-               profile.birthYear = 1999
-               profile.setAvatar(avatar)
-               whenever(currentSone.profile).thenReturn(profile)
-       }
-
-       @Test
-       fun `get request stores fields of current sone’s profile in template context`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["firstName"], equalTo<Any>("First"))
-               assertThat(templateContext["middleName"], equalTo<Any>("Middle"))
-               assertThat(templateContext["lastName"], equalTo<Any>("Last"))
-               assertThat(templateContext["birthDay"], equalTo<Any>(31))
-               assertThat(templateContext["birthMonth"], equalTo<Any>(12))
-               assertThat(templateContext["birthYear"], equalTo<Any>(1999))
-               assertThat(templateContext["avatarId"], equalTo<Any>("image-id"))
-               assertThat(templateContext["fields"], equalTo<Any>(listOf(firstField, secondField)))
-       }
-
-       @Test
-       fun `post request without any command stores fields of current sone’s profile in template context`() {
-               request("", POST)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["firstName"], equalTo<Any>("First"))
-               assertThat(templateContext["middleName"], equalTo<Any>("Middle"))
-               assertThat(templateContext["lastName"], equalTo<Any>("Last"))
-               assertThat(templateContext["birthDay"], equalTo<Any>(31))
-               assertThat(templateContext["birthMonth"], equalTo<Any>(12))
-               assertThat(templateContext["birthYear"], equalTo<Any>(1999))
-               assertThat(templateContext["avatarId"], equalTo<Any>("image-id"))
-               assertThat(templateContext["fields"], equalTo<Any>(listOf(firstField, secondField)))
-       }
-
-       private fun <T> verifySingleFieldCanBeChanged(fieldName: String, newValue: T, expectedValue: T = newValue, fieldAccessor: () -> T) {
-               request("", POST)
-               addHttpRequestParameter("save-profile", "true")
-               addHttpRequestParameter(fieldName, newValue.toString())
-               verifyRedirect("editProfile.html") {
-                       verify(core).touchConfiguration()
-                       assertThat(fieldAccessor(), equalTo(expectedValue))
-               }
-       }
-
-       @Test
-       fun `post request with new first name and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("first-name", "New First") { profile.firstName }
-       }
-
-       @Test
-       fun `post request with new middle name and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("middle-name", "New Middle") { profile.middleName }
-       }
-
-       @Test
-       fun `post request with new last name and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("last-name", "New Last") { profile.lastName }
-       }
-
-       @Test
-       fun `post request with new birth day and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("birth-day", 1) { profile.birthDay }
-       }
-
-       @Test
-       fun `post request with new birth month and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("birth-month", 1) { profile.birthMonth }
-       }
-
-       @Test
-       fun `post request with new birth year and save profile saves the profile and redirects back to profile edit page`() {
-               verifySingleFieldCanBeChanged("birth-year", 1) { profile.birthYear }
-       }
-
-       @Test
-       fun `post request with new avatar ID and save profile saves the profile and redirects back to profile edit page`() {
-               val newAvatar = mock<Image>()
-               whenever(newAvatar.sone).thenReturn(currentSone)
-               whenever(newAvatar.id).thenReturn("avatar-id")
-               addImage("avatar-id", newAvatar)
-               verifySingleFieldCanBeChanged("avatarId", "avatar-id") { profile.avatar }
-       }
-
-       @Test
-       fun `post request with field value saves profile and redirects back to profile edit page`() {
-               val field = profile.addField("name")
-               field.value = "old"
-               verifySingleFieldCanBeChanged("field-${field.id}", "new") { profile.getFieldByName("name")!!.value }
-       }
-
-       @Test
-       fun `post request with field value saves filtered value to profile and redirects back to profile edit page`() {
-               val field = profile.addField("name")
-               field.value = "old"
-               addHttpRequestHeader("Host", "www.te.st")
-               verifySingleFieldCanBeChanged("field-${field.id}", "http://www.te.st/KSK@GPL.txt", "KSK@GPL.txt") { profile.getFieldByName("name")!!.value }
-       }
-
-       @Test
-       fun `adding a field with a duplicate name sets error in template context`() {
-               request("", POST)
-               profile.addField("new-field")
-               addHttpRequestParameter("add-field", "true")
-               addHttpRequestParameter("field-name", "new-field")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["fieldName"], equalTo<Any>("new-field"))
-               assertThat(templateContext["duplicateFieldName"], equalTo<Any>(true))
-               verify(core, never()).touchConfiguration()
-       }
-
-       @Test
-       fun `adding a field with a new name sets adds field to profile and redirects to profile edit page`() {
-               request("", POST)
-               addHttpRequestParameter("add-field", "true")
-               addHttpRequestParameter("field-name", "new-field")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(profile.getFieldByName("new-field"), notNullValue())
-                       verify(currentSone).profile = profile
-                       verify(core).touchConfiguration()
-               }
-       }
-
-       @Test
-       fun `deleting a field redirects to delete field page`() {
-               request("", POST)
-               addHttpRequestParameter("delete-field-${firstField.id}", "true")
-               verifyRedirect("deleteProfileField.html?field=${firstField.id}")
-       }
-
-       @Test
-       fun `moving a field up moves the field up and redirects to the edit profile page`() {
-               request("", POST)
-               addHttpRequestParameter("move-up-field-${secondField.id}", "true")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(profile.fields, contains(secondField, firstField))
-                       verify(currentSone).profile = profile
-               }
-       }
-
-       @Test
-       fun `moving an invalid field up redirects to the invalid page`() {
-               request("", POST)
-               addHttpRequestParameter("move-up-field-foo", "true")
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `moving a field down moves the field down and redirects to the edit profile page`() {
-               request("", POST)
-               addHttpRequestParameter("move-down-field-${firstField.id}", "true")
-               verifyRedirect("editProfile.html#profile-fields") {
-                       assertThat(profile.fields, contains(secondField, firstField))
-                       verify(currentSone).profile = profile
-               }
-       }
-
-       @Test
-       fun `moving an invalid field down redirects to the invalid page`() {
-               request("", POST)
-               addHttpRequestParameter("move-down-field-foo", "true")
-               verifyRedirect("invalid.html")
-       }
-
-       @Test
-       fun `editing a field redirects to the edit profile page`() {
-               request("", POST)
-               addHttpRequestParameter("edit-field-${firstField.id}", "true")
-               verifyRedirect("editProfileField.html?field=${firstField.id}")
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/FollowSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/FollowSonePageTest.kt
deleted file mode 100644 (file)
index 1f6fc26..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.ArgumentMatchers
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [FollowSonePage].
- */
-class FollowSonePageTest : WebPageTest() {
-
-       private val page = FollowSonePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `a single sone can be followed`() {
-               request("", POST)
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               addHttpRequestParameter("sone", "sone-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core).followSone(currentSone, "sone-id")
-                       verify(core).markSoneKnown(sone)
-               }
-       }
-
-       @Test
-       fun `multiple sones can be followed`() {
-               request("", POST)
-               val firstSone = mock<Sone>()
-               addSone("sone-id1", firstSone)
-               val secondSone = mock<Sone>()
-               addSone("sone-id2", secondSone)
-               addHttpRequestParameter("sone", "sone-id1,sone-id2")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core).followSone(currentSone, "sone-id1")
-                       verify(core).followSone(currentSone, "sone-id2")
-                       verify(core).markSoneKnown(firstSone)
-                       verify(core).markSoneKnown(secondSone)
-               }
-       }
-
-       @Test
-       fun `a non-existing sone is not followed`() {
-               request("", POST)
-               addHttpRequestParameter("sone", "sone-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).followSone(ArgumentMatchers.eq(currentSone), anyString())
-                       verify(core, never()).markSoneKnown(any<Sone>())
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/GetImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/GetImagePageTest.kt
deleted file mode 100644 (file)
index 57f887a..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.TemporaryImage
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-
-/**
- * Unit test for [GetImagePage].
- */
-class GetImagePageTest : WebPageTest() {
-
-       private val page = GetImagePage(webInterface)
-
-       @Test
-       fun `page returns correct path`() {
-               assertThat(page.path, equalTo("getImage.html"))
-       }
-
-       @Test
-       fun `page is not a prefix page`() {
-               assertThat(page.isPrefixPage, equalTo(false))
-       }
-
-       @Test
-       fun `page is not link-excepted`() {
-               assertThat(page.isLinkExcepted(null), equalTo(false))
-       }
-
-       @Test
-       fun `invalid image returns 404 response`() {
-               page.handleRequest(freenetRequest, response)
-               assertThat(response.statusCode, equalTo(404))
-               assertThat(responseBytes, equalTo(ByteArray(0)))
-       }
-
-       @Test
-       fun `valid image returns response with correct data`() {
-               val image = TemporaryImage("temp-id").apply {
-                       mimeType = "image/test"
-                       imageData = ByteArray(5, Int::toByte)
-               }
-               addHttpRequestParameter("image", "temp-id")
-               addTemporaryImage("temp-id", image)
-               page.handleRequest(freenetRequest, response)
-               assertThat(response.statusCode, equalTo(200))
-               assertThat(response.contentType, equalTo("image/test"))
-               assertThat(responseBytes, equalTo(ByteArray(5, Int::toByte)))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ImageBrowserPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ImageBrowserPageTest.kt
deleted file mode 100644 (file)
index 426eaf4..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-
-/**
- * Unit test for [ImageBrowserPage].
- */
-class ImageBrowserPageTest : WebPageTest() {
-
-       private val page = ImageBrowserPage(template, webInterface)
-
-       @Test
-       fun `get request with album sets album and page in template context`() {
-               request("", GET)
-               val album = mock<Album>()
-               addAlbum("album-id", album)
-               addHttpRequestParameter("album", "album-id")
-               addHttpRequestParameter("page", "5")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["albumRequested"], equalTo<Any>(true))
-               assertThat(templateContext["album"], equalTo<Any>(album))
-               assertThat(templateContext["page"], equalTo<Any>("5"))
-       }
-
-       @Test
-       fun `get request with image sets image in template context`() {
-               request("", GET)
-               val image = mock<Image>()
-               addImage("image-id", image)
-               addHttpRequestParameter("image", "image-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["imageRequested"], equalTo<Any>(true))
-               assertThat(templateContext["image"], equalTo<Any>(image))
-       }
-
-       @Test
-       fun `get request with sone sets sone in template context`() {
-               request("", GET)
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               addHttpRequestParameter("sone", "sone-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["soneRequested"], equalTo<Any>(true))
-               assertThat(templateContext["sone"], equalTo<Any>(sone))
-       }
-
-       @Test
-       fun `get request with mode of gallery sets albums and page in template context`() {
-               request("", GET)
-               val firstSone = createSone("first album", "second album")
-               addSone("sone1", firstSone)
-               val secondSone = createSone("third album", "fourth album")
-               addSone("sone2", secondSone)
-               addHttpRequestParameter("mode", "gallery")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["galleryRequested"], equalTo<Any>(true))
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext["albums"] as Iterable<Album>, contains(
-                               firstSone.rootAlbum.albums[0],
-                               secondSone.rootAlbum.albums[1],
-                               firstSone.rootAlbum.albums[1],
-                               secondSone.rootAlbum.albums[0]
-               ))
-       }
-
-       private fun createSone(firstAlbumTitle: String, secondAlbumTitle: String): Sone {
-               return mock<Sone>().apply {
-                       val rootAlbum = mock<Album>()
-                       val firstAlbum = mock<Album>()
-                       val firstImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
-                       whenever(firstAlbum.images).thenReturn(listOf(firstImage))
-                       val secondAlbum = mock<Album>()
-                       val secondImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
-                       whenever(secondAlbum.images).thenReturn(listOf(secondImage))
-                       whenever(firstAlbum.title).thenReturn(firstAlbumTitle)
-                       whenever(secondAlbum.title).thenReturn(secondAlbumTitle)
-                       whenever(rootAlbum.albums).thenReturn(listOf(firstAlbum, secondAlbum))
-                       whenever(this.rootAlbum).thenReturn(rootAlbum)
-               }
-       }
-
-       @Test
-       fun `requesting nothing will show the albums of the current sone`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["soneRequested"], equalTo<Any>(true))
-               assertThat(templateContext["sone"], equalTo<Any>(currentSone))
-       }
-
-       @Test
-       fun `page is link-excepted`() {
-           assertThat(page.isLinkExcepted(null), equalTo(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/IndexPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/IndexPageTest.kt
deleted file mode 100644 (file)
index 414c0f2..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-package net.pterodactylus.sone.web
-
-import com.google.common.base.Optional.fromNullable
-import com.google.common.base.Predicate
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.notify.PostVisibilityFilter
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.junit.Before
-import org.junit.Test
-import org.mockito.ArgumentMatchers
-
-/**
- * Unit test for [IndexPage].
- */
-class IndexPageTest : WebPageTest() {
-
-       private val postVisibilityFilter = mock<PostVisibilityFilter>()
-       private val page = IndexPage(template, webInterface, postVisibilityFilter)
-
-       @Before
-       fun setupPostVisibilityFilter() {
-               whenever(postVisibilityFilter.isVisible(ArgumentMatchers.eq(currentSone))).thenReturn(object : Predicate<Post> {
-                       override fun apply(input: Post?) = true
-               })
-       }
-
-       private fun createPost(time: Long, directed: Boolean = false) = mock<Post>().apply {
-               whenever(this.time).thenReturn(time)
-               whenever(recipient).thenReturn(fromNullable(if (directed) currentSone else null))
-       }
-
-       @Test
-       fun `index page shows all posts of current sone`() {
-               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
-               whenever(currentSone.posts).thenReturn(posts)
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext["posts"] as Iterable<Post>, contains(*posts.toTypedArray()))
-       }
-
-       @Test
-       fun `index page shows posts directed at current sone from non-followed sones`() {
-               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
-               whenever(currentSone.posts).thenReturn(posts)
-               val notFollowedSone = mock<Sone>()
-               val notFollowedPosts = listOf(createPost(2500, true), createPost(1500))
-               whenever(notFollowedSone.posts).thenReturn(notFollowedPosts)
-               addSone("notfollowed1", notFollowedSone)
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext["posts"] as Iterable<Post>, contains(
-                               posts[0], notFollowedPosts[0], posts[1], posts[2]
-               ))
-       }
-
-       @Test
-       fun `index page does not show duplicate posts`() {
-               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
-               whenever(currentSone.posts).thenReturn(posts)
-               val followedSone = mock<Sone>()
-               val followedPosts = listOf(createPost(2500, true), createPost(1500))
-               whenever(followedSone.posts).thenReturn(followedPosts)
-               whenever(currentSone.friends).thenReturn(listOf("followed1", "followed2"))
-               addSone("followed1", followedSone)
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext["posts"] as Iterable<Post>, contains(
-                               posts[0], followedPosts[0], posts[1], followedPosts[1], posts[2]
-               ))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/KnownSonesPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/KnownSonesPageTest.kt
deleted file mode 100644 (file)
index f0ee9db..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-package net.pterodactylus.sone.web
-
-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.Sone
-import net.pterodactylus.sone.freenet.wot.Identity
-import net.pterodactylus.sone.freenet.wot.OwnIdentity
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.junit.Before
-import org.junit.Test
-
-/**
- * Unit test for [KnownSonesPage].
- */
-class KnownSonesPageTest : WebPageTest() {
-
-       private val page = KnownSonesPage(template, webInterface)
-
-       private val sones = listOf(
-                       createSone(1000, 4, 7, 2, "sone2", true, true),
-                       createSone(2000, 3, 2, 3, "Sone1", false, true),
-                       createSone(3000, 3, 8, 1, "Sone3", true, false),
-                       createSone(4000, 1, 6, 0, "sone0", false, false)
-       )
-
-       @Before
-       fun setupSones() {
-               addSone("sone1", sones[0])
-               addSone("sone2", sones[1])
-               addSone("sone3", sones[2])
-               addSone("sone4", sones[3])
-       }
-
-       private fun createSone(time: Long, posts: Int, replies: Int, images: Int, name: String, local: Boolean, new: Boolean) = mock<Sone>().apply {
-               whenever(identity).thenReturn(if (local) mock<OwnIdentity>() else mock<Identity>())
-               whenever(isKnown).thenReturn(!new)
-               whenever(this.time).thenReturn(time)
-               whenever(this.posts).thenReturn((0..(posts - 1)).map { mock<Post>() })
-               whenever(this.replies).thenReturn((0..(replies - 1)).map { mock<PostReply>() }.toSet())
-               val album = mock<Album>()
-               whenever(album.images).thenReturn(((0..(images - 1)).map { mock<Image>() }))
-               val rootAlbum = mock<Album>().apply {
-                       whenever(albums).thenReturn(listOf(album))
-               }
-               whenever(this.rootAlbum).thenReturn(rootAlbum)
-               whenever(this.profile).thenReturn(mock<Profile>())
-               whenever(id).thenReturn(name.toLowerCase())
-               whenever(this.name).thenReturn(name)
-       }
-
-       private fun verifySonesAreInOrder(vararg indices: Int) {
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext["knownSones"] as Iterable<Sone>, contains(
-                               *indices.map { sones[it] }.toTypedArray()
-               ))
-       }
-
-       @Test
-       fun `default known sones are sorted newest first`() {
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 2, 1, 0)
-       }
-
-       @Test
-       fun `known sones can be sorted by oldest first`() {
-               addHttpRequestParameter("order", "asc")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(0, 1, 2, 3)
-       }
-
-       @Test
-       fun `known sones can be sorted by posts, most posts first`() {
-               addHttpRequestParameter("sort", "posts")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(0, 2, 1, 3)
-       }
-
-       @Test
-       fun `known sones can be sorted by posts, least posts first`() {
-               addHttpRequestParameter("sort", "posts")
-               addHttpRequestParameter("order", "asc")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 1, 2, 0)
-       }
-
-       @Test
-       fun `known sones can be sorted by images, most images first`() {
-               addHttpRequestParameter("sort", "images")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(1, 0, 2, 3)
-       }
-
-       @Test
-       fun `known sones can be sorted by images, least images first`() {
-               addHttpRequestParameter("sort", "images")
-               addHttpRequestParameter("order", "asc")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 2, 0, 1)
-       }
-
-       @Test
-       fun `known sones can be sorted by nice name, ascending`() {
-               addHttpRequestParameter("sort", "name")
-               addHttpRequestParameter("order", "asc")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 1, 0, 2)
-       }
-
-       @Test
-       fun `known sones can be sorted by nice name, descending`() {
-               addHttpRequestParameter("sort", "name")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(2, 0, 1, 3)
-       }
-
-       @Test
-       fun `known sones can be filtered by local sones`() {
-               addHttpRequestParameter("filter", "own")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(2, 0)
-       }
-
-       @Test
-       fun `known sones can be filtered by non-local sones`() {
-               addHttpRequestParameter("filter", "not-own")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 1)
-       }
-
-       @Test
-       fun `known sones can be filtered by new sones`() {
-               addHttpRequestParameter("filter", "new")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(1, 0)
-       }
-
-       @Test
-       fun `known sones can be filtered by known sones`() {
-               addHttpRequestParameter("filter", "not-new")
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 2)
-       }
-
-       @Test
-       fun `known sones can be filtered by followed sones`() {
-               addHttpRequestParameter("filter", "followed")
-               listOf("sone1", "sone3").forEach { whenever(currentSone.hasFriend(it)).thenReturn(true) }
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(2, 1)
-       }
-
-       @Test
-       fun `known sones can be filtered by not-followed sones`() {
-               addHttpRequestParameter("filter", "not-followed")
-               listOf("sone1", "sone3").forEach { whenever(currentSone.hasFriend(it)).thenReturn(true) }
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 0)
-       }
-
-       @Test
-       fun `known sones can not be filtered by followed sones if there is no current sone`() {
-               addHttpRequestParameter("filter", "followed")
-               unsetCurrentSone()
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 2, 1, 0)
-       }
-
-       @Test
-       fun `known sones can not be filtered by not-followed sones if there is no current sone`() {
-               addHttpRequestParameter("filter", "not-followed")
-               unsetCurrentSone()
-               page.handleRequest(freenetRequest, templateContext)
-               verifySonesAreInOrder(3, 2, 1, 0)
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/LikePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/LikePageTest.kt
deleted file mode 100644 (file)
index 52f6174..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-/**
- * Unit test for [LikePage].
- */
-class LikePageTest : WebPageTest() {
-
-       private val page = LikePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with post id likes post and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("type", "post")
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(currentSone).addLikedPostId("post-id")
-               }
-       }
-
-       @Test
-       fun `post request with reply id likes post and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("type", "reply")
-               addHttpRequestParameter("reply", "reply-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(currentSone).addLikedReplyId("reply-id")
-               }
-       }
-
-       @Test
-       fun `post request with invalid likes redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("type", "foo")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verifyNoMoreInteractions(currentSone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/LockSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/LockSonePageTest.kt
deleted file mode 100644 (file)
index 7136e04..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [LockSonePage].
- */
-class LockSonePageTest : WebPageTest() {
-
-       private val page = LockSonePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `locking an invalid local sone redirects to return page`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).lockSone(any<Sone>())
-               }
-       }
-
-       @Test
-       fun `locking an valid local sone locks the sone and redirects to return page`() {
-               addHttpRequestParameter("sone", "sone-id")
-               val sone = mock<Sone>()
-               addLocalSone("sone-id", sone)
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core).lockSone(sone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/LoginPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/LoginPageTest.kt
deleted file mode 100644 (file)
index 1dbe3cd..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.freenet.wot.Identity
-import net.pterodactylus.sone.freenet.wot.OwnIdentity
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.thenReturnMock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.containsInAnyOrder
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [LoginPage].
- */
-class LoginPageTest : WebPageTest() {
-
-       private val page = LoginPage(template, webInterface)
-
-       private val sones = listOf(createSone("Sone", "Test"), createSone("Test"), createSone("Sone"))
-
-       override fun getPage() = page
-
-       private fun createSone(vararg contexts: String) = mock<Sone>().apply {
-               whenever(id).thenReturn(hashCode().toString())
-               val identity = mock<OwnIdentity>().apply {
-                       whenever(this.contexts).thenReturn(contexts.toSet())
-                       contexts.forEach { whenever(hasContext(it)).thenReturn(true) }
-               }
-               whenever(this.identity).thenReturn(identity)
-               whenever(profile).thenReturnMock()
-       }
-
-       @Before
-       fun setupSones() {
-               addLocalSone("sone1", sones[0])
-               addLocalSone("sone2", sones[1])
-               addLocalSone("sone3", sones[2])
-               addOwnIdentity(sones[0].identity as OwnIdentity)
-               addOwnIdentity(sones[1].identity as OwnIdentity)
-               addOwnIdentity(sones[2].identity as OwnIdentity)
-       }
-
-       @Test
-       fun `page returns correct path`() {
-           assertThat(page.path, equalTo("login.html"))
-       }
-
-       @Test
-       fun `page does not require login`() {
-           assertThat(page.requiresLogin(), equalTo(false))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `get request stores sones in template context`() {
-               request("", GET)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["sones"] as Iterable<Sone>, containsInAnyOrder(sones[0], sones[1], sones[2]))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `get request stores identities without sones in template context`() {
-               request("", GET)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["identitiesWithoutSone"] as Iterable<Identity>, contains(sones[1].identity))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `post request with invalid sone sets sones and identities without sone in template context`() {
-               request("", POST)
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(templateContext["sones"] as Iterable<Sone>, containsInAnyOrder(sones[0], sones[1], sones[2]))
-               assertThat(templateContext["identitiesWithoutSone"] as Iterable<Identity>, contains(sones[1].identity))
-       }
-
-       @Test
-       fun `post request with valid sone logs in the sone and redirects to index page`() {
-               request("", POST)
-               addHttpRequestParameter("sone-id", "sone2")
-               verifyRedirect("index.html") {
-                       verify(webInterface).setCurrentSone(toadletContext, sones[1])
-               }
-       }
-
-       @Test
-       fun `post request with valid sone and target redirects to target page`() {
-               request("", POST)
-               addHttpRequestParameter("sone-id", "sone2")
-               addHttpRequestParameter("target", "foo.html")
-               verifyRedirect("foo.html") {
-                       verify(webInterface).setCurrentSone(toadletContext, sones[1])
-               }
-       }
-
-       @Test
-       fun `redirect to index html if a sone is logged in`() {
-               assertThat(page.getRedirectTarget(freenetRequest), equalTo("index.html"))
-       }
-
-       @Test
-       fun `do not redirect if no sone is logged in`() {
-               unsetCurrentSone()
-               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
-       }
-
-       @Test
-       fun `page is not enabled if full access required and request is not full access`() {
-               core.preferences.isRequireFullAccess = true
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is enabled if no full access is required and there is no current sone`() {
-               unsetCurrentSone()
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `page is not enabled if no full access is required but there is a current sone`() {
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is enabled if full access required and request is full access and there is no current sone`() {
-               core.preferences.isRequireFullAccess = true
-               unsetCurrentSone()
-               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `page is not enabled if full access required and request is full access but there is a current sone`() {
-               core.preferences.isRequireFullAccess = true
-               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/LogoutPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/LogoutPageTest.kt
deleted file mode 100644 (file)
index 8120b50..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [LogoutPage].
- */
-class LogoutPageTest : WebPageTest() {
-
-       private val page = LogoutPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `page unsets current sone and redirects to index`() {
-               verifyRedirect("index.html") {
-                       verify(webInterface).setCurrentSone(toadletContext, null)
-               }
-       }
-
-       @Test
-       fun `page is not enabled if sone requires full access and request does not have full access`() {
-               core.preferences.isRequireFullAccess = true
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is disabled if no sone is logged in`() {
-               unsetCurrentSone()
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is disabled if sone is logged in but there is only one sone`() {
-               whenever(core.localSones).thenReturn(listOf(currentSone))
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is enabled if sone is logged in and there is more than one sone`() {
-               whenever(core.localSones).thenReturn(listOf(currentSone, currentSone))
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `page is enabled if full access is required and present and sone is logged in and there is more than one sone`() {
-               core.preferences.isRequireFullAccess = true
-               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
-               whenever(core.localSones).thenReturn(listOf(currentSone, currentSone))
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/MarkAsKnownPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/MarkAsKnownPageTest.kt
deleted file mode 100644 (file)
index c44bbd4..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.PostReply
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [MarkAsKnownPage].
- */
-class MarkAsKnownPageTest : WebPageTest() {
-
-       private val page = MarkAsKnownPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `posts can be marked as known`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("type", "post")
-               addHttpRequestParameter("id", "post1 post2 post3")
-               val posts = listOf(mock<Post>(), mock<Post>())
-               addPost("post1", posts[0])
-               addPost("post3", posts[1])
-               verifyRedirect("return.html") {
-                       verify(core).markPostKnown(posts[0])
-                       verify(core).markPostKnown(posts[1])
-               }
-       }
-
-       @Test
-       fun `replies can be marked as known`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("type", "reply")
-               addHttpRequestParameter("id", "reply1 reply2 reply3")
-               val replies = listOf(mock<PostReply>(), mock<PostReply>())
-               addPostReply("reply1", replies[0])
-               addPostReply("reply3", replies[1])
-               verifyRedirect("return.html") {
-                       verify(core).markReplyKnown(replies[0])
-                       verify(core).markReplyKnown(replies[1])
-               }
-       }
-
-       @Test
-       fun `sones can be marked as known`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("type", "sone")
-               addHttpRequestParameter("id", "sone1 sone2 sone3")
-               val sones = listOf(mock<Sone>(), mock<Sone>())
-               addSone("sone1", sones[0])
-               addSone("sone3", sones[1])
-               verifyRedirect("return.html") {
-                       verify(core).markSoneKnown(sones[0])
-                       verify(core).markSoneKnown(sones[1])
-               }
-       }
-
-       @Test
-       fun `different type redirects to invalid page`() {
-               addHttpRequestParameter("type", "foo")
-               verifyRedirect("invalid.html")
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/OptionsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/OptionsPageTest.kt
deleted file mode 100644 (file)
index cb8a3e5..0000000
+++ /dev/null
@@ -1,340 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
-import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.FOLLOWED
-import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.hasItem
-import org.hamcrest.Matchers.nullValue
-import org.junit.Before
-import org.junit.Test
-
-/**
- * Unit test for [OptionsPage].
- */
-class OptionsPageTest : WebPageTest() {
-
-       private val page = OptionsPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Before
-       fun setupPreferences() {
-               core.preferences.insertionDelay = 1
-               core.preferences.charactersPerPost = 50
-               core.preferences.fcpFullAccessRequired = WRITING
-               core.preferences.imagesPerPage = 4
-               core.preferences.isFcpInterfaceActive = true
-               core.preferences.isRequireFullAccess = true
-               core.preferences.negativeTrust = 7
-               core.preferences.positiveTrust = 8
-               core.preferences.postCutOffLength = 51
-               core.preferences.postsPerPage = 10
-               core.preferences.trustComment = "11"
-       }
-
-       @Before
-       fun setupSoneOptions() {
-               whenever(currentSone.options).thenReturn(DefaultSoneOptions().apply {
-                       isAutoFollow = true
-                       isShowNewPostNotifications = true
-                       isShowNewReplyNotifications = true
-                       isShowNewSoneNotifications = true
-                       isSoneInsertNotificationEnabled = true
-                       loadLinkedImages = FOLLOWED
-                       showCustomAvatars = FOLLOWED
-               })
-       }
-
-       @Test
-       fun `get request stores all preferences in the template context`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["auto-follow"], equalTo<Any>(true))
-               assertThat(templateContext["show-notification-new-sones"], equalTo<Any>(true))
-               assertThat(templateContext["show-notification-new-posts"], equalTo<Any>(true))
-               assertThat(templateContext["show-notification-new-replies"], equalTo<Any>(true))
-               assertThat(templateContext["enable-sone-insert-notifications"], equalTo<Any>(true))
-               assertThat(templateContext["load-linked-images"], equalTo<Any>("FOLLOWED"))
-               assertThat(templateContext["show-custom-avatars"], equalTo<Any>("FOLLOWED"))
-               assertThat(templateContext["insertion-delay"], equalTo<Any>(1))
-               assertThat(templateContext["characters-per-post"], equalTo<Any>(50))
-               assertThat(templateContext["fcp-full-access-required"], equalTo<Any>(1))
-               assertThat(templateContext["images-per-page"], equalTo<Any>(4))
-               assertThat(templateContext["fcp-interface-active"], equalTo<Any>(true))
-               assertThat(templateContext["require-full-access"], equalTo<Any>(true))
-               assertThat(templateContext["negative-trust"], equalTo<Any>(7))
-               assertThat(templateContext["positive-trust"], equalTo<Any>(8))
-               assertThat(templateContext["post-cut-off-length"], equalTo<Any>(51))
-               assertThat(templateContext["posts-per-page"], equalTo<Any>(10))
-               assertThat(templateContext["trust-comment"], equalTo<Any>("11"))
-       }
-
-       @Test
-       fun `get request without sone does not store sone-specific preferences in the template context`() {
-               request("", GET)
-               unsetCurrentSone()
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["auto-follow"], nullValue())
-               assertThat(templateContext["show-notification-new-sones"], nullValue())
-               assertThat(templateContext["show-notification-new-posts"], nullValue())
-               assertThat(templateContext["show-notification-new-replies"], nullValue())
-               assertThat(templateContext["enable-sone-insert-notifications"], nullValue())
-               assertThat(templateContext["load-linked-images"], nullValue())
-               assertThat(templateContext["show-custom-avatars"], nullValue())
-       }
-
-       private fun <T> verifyThatOptionCanBeSet(option: String, setValue: Any?, expectedValue: T, getter: () -> T) {
-               request("", POST)
-               addHttpRequestParameter(option, setValue.toString())
-               addHttpRequestParameter("show-custom-avatars", "ALWAYS")
-               addHttpRequestParameter("load-linked-images", "ALWAYS")
-               verifyRedirect("options.html") {
-                       assertThat(getter(), equalTo(expectedValue))
-               }
-       }
-
-       @Test
-       fun `auto-follow option can be set`() {
-               verifyThatOptionCanBeSet("auto-follow", "checked", true) { currentSone.options.isAutoFollow }
-       }
-
-       @Test
-       fun `show new sone notification option can be set`() {
-               verifyThatOptionCanBeSet("show-notification-new-sones", "checked", true) { currentSone.options.isShowNewSoneNotifications }
-       }
-
-       @Test
-       fun `show new post notification option can be set`() {
-               verifyThatOptionCanBeSet("show-notification-new-posts", "checked", true) { currentSone.options.isShowNewPostNotifications }
-       }
-
-       @Test
-       fun `show new reply notification option can be set`() {
-               verifyThatOptionCanBeSet("show-notification-new-replies", "checked", true) { currentSone.options.isShowNewReplyNotifications }
-       }
-
-       @Test
-       fun `enable sone insert notifications option can be set`() {
-               verifyThatOptionCanBeSet("enable-sone-insert-notifications", "checked", true) { currentSone.options.isSoneInsertNotificationEnabled }
-       }
-
-       @Test
-       fun `load linked images option can be set`() {
-               verifyThatOptionCanBeSet("load-linked-images", "TRUSTED", TRUSTED) { currentSone.options.loadLinkedImages }
-       }
-
-       @Test
-       fun `show custom avatar option can be set`() {
-               verifyThatOptionCanBeSet("show-custom-avatars", "TRUSTED", TRUSTED) { currentSone.options.showCustomAvatars }
-       }
-
-       private fun verifyThatWrongValueForPreferenceIsDetected(name: String, value: String) {
-               unsetCurrentSone()
-               request("", POST)
-               addHttpRequestParameter(name, value)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["fieldErrors"] as Iterable<*>, hasItem(name))
-       }
-
-       private fun <T> verifyThatPreferencesCanBeSet(name: String, setValue: String?, expectedValue: T, getter: () -> T) {
-               unsetCurrentSone()
-               request("", POST)
-               addHttpRequestParameter(name, setValue)
-               verifyRedirect("options.html") {
-                       assertThat(getter(), equalTo(expectedValue))
-               }
-       }
-
-       @Test
-       fun `insertion delay can not be set to less than 0 seconds`() {
-               verifyThatWrongValueForPreferenceIsDetected("insertion-delay", "-1")
-       }
-
-       @Test
-       fun `insertion delay can be set to 0 seconds`() {
-               verifyThatPreferencesCanBeSet("insertion-delay", "0", 0) { core.preferences.insertionDelay }
-       }
-
-       @Test
-       fun `setting insertion to an invalid value will reset it`() {
-               verifyThatPreferencesCanBeSet("insertion-delay", "foo", 60) { core.preferences.insertionDelay }
-       }
-
-       @Test
-       fun `characters per post can not be set to less than -1`() {
-               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "-2")
-       }
-
-       @Test
-       fun `characters per post can be set to -1`() {
-               verifyThatPreferencesCanBeSet("characters-per-post", "-1", -1) { core.preferences.charactersPerPost }
-       }
-
-       @Test
-       fun `characters per post can not be set to 0`() {
-               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "0")
-       }
-
-       @Test
-       fun `characters per post can not be set to 49`() {
-               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "49")
-       }
-
-       @Test
-       fun `characters per post can be set to 50`() {
-               verifyThatPreferencesCanBeSet("characters-per-post", "50", 50) { core.preferences.charactersPerPost }
-       }
-
-       @Test
-       fun `fcp full acess required option can be set to always`() {
-               verifyThatPreferencesCanBeSet("fcp-full-access-required", "2", FullAccessRequired.ALWAYS) { core.preferences.fcpFullAccessRequired }
-       }
-
-       @Test
-       fun `fcp full acess required option can be set to writing`() {
-               verifyThatPreferencesCanBeSet("fcp-full-access-required", "1", FullAccessRequired.WRITING) { core.preferences.fcpFullAccessRequired }
-       }
-
-       @Test
-       fun `fcp full acess required option can be set to no`() {
-               verifyThatPreferencesCanBeSet("fcp-full-access-required", "0", FullAccessRequired.NO) { core.preferences.fcpFullAccessRequired }
-       }
-
-       @Test
-       fun `fcp full acess required option is not changed if invalid value is set`() {
-               verifyThatPreferencesCanBeSet("fcp-full-access-required", "foo", FullAccessRequired.WRITING) { core.preferences.fcpFullAccessRequired }
-       }
-
-       @Test
-       fun `images per page can not be set to 0`() {
-               verifyThatWrongValueForPreferenceIsDetected("images-per-page", "0")
-       }
-
-       @Test
-       fun `images per page can be set to 1`() {
-               verifyThatPreferencesCanBeSet("images-per-page", "1", 1) { core.preferences.imagesPerPage }
-       }
-
-       @Test
-       fun `images per page is set to 9 if invalid value is requested`() {
-               verifyThatPreferencesCanBeSet("images-per-page", "foo", 9) { core.preferences.imagesPerPage }
-       }
-
-       @Test
-       fun `fcp interface can be set to true`() {
-               verifyThatPreferencesCanBeSet("fcp-interface-active", "checked", true) { core.preferences.isFcpInterfaceActive }
-       }
-
-       @Test
-       fun `fcp interface can be set to false`() {
-               verifyThatPreferencesCanBeSet("fcp-interface-active", null, false) { core.preferences.isFcpInterfaceActive }
-       }
-
-       @Test
-       fun `require full access can be set to true`() {
-               verifyThatPreferencesCanBeSet("require-full-access", "checked", true) { core.preferences.isRequireFullAccess }
-       }
-
-       @Test
-       fun `require full access can be set to false`() {
-               verifyThatPreferencesCanBeSet("require-full-access", null, false) { core.preferences.isRequireFullAccess }
-       }
-
-       @Test
-       fun `negative trust can not be set to -101`() {
-               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "-101")
-       }
-
-       @Test
-       fun `negative trust can be set to -100`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "-100", -100) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `negative trust can be set to 100`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "100", 100) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `negative trust can not be set to 101`() {
-               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "101")
-       }
-
-       @Test
-       fun `negative trust is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("negative-trust", "invalid", -25) { core.preferences.negativeTrust }
-       }
-
-       @Test
-       fun `positive trust can not be set to -1`() {
-               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "-1")
-       }
-
-       @Test
-       fun `positive trust can be set to 0`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "0", 0) { core.preferences.positiveTrust }
-       }
-
-       @Test
-       fun `positive trust can be set to 100`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "100", 100) { core.preferences.positiveTrust }
-       }
-
-       @Test
-       fun `positive trust can not be set to 101`() {
-               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "101")
-       }
-
-       @Test
-       fun `positive trust is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("positive-trust", "invalid", 75) { core.preferences.positiveTrust }
-       }
-
-       @Test
-       fun `post cut off length can not be set to -49`() {
-               verifyThatWrongValueForPreferenceIsDetected("post-cut-off-length", "-49")
-       }
-
-       @Test
-       fun `post cut off length can be set to 50`() {
-               verifyThatPreferencesCanBeSet("post-cut-off-length", "50", 50) { core.preferences.postCutOffLength }
-       }
-
-       @Test
-       fun `post cut off length is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("post-cut-off-length", "invalid", 200) { core.preferences.postCutOffLength }
-       }
-
-       @Test
-       fun `posts per page can not be set to 0`() {
-               verifyThatWrongValueForPreferenceIsDetected("posts-per-page", "-49")
-       }
-
-       @Test
-       fun `posts per page can be set to 1`() {
-               verifyThatPreferencesCanBeSet("posts-per-page", "1", 1) { core.preferences.postsPerPage }
-       }
-
-       @Test
-       fun `posts per page is set to default on invalid value`() {
-               verifyThatPreferencesCanBeSet("posts-per-page", "invalid", 10) { core.preferences.postsPerPage }
-       }
-
-       @Test
-       fun `trust comment can be set`() {
-               verifyThatPreferencesCanBeSet("trust-comment", "trust", "trust") { core.preferences.trustComment }
-       }
-
-       @Test
-       fun `trust comment is set to default when set to empty value`() {
-               verifyThatPreferencesCanBeSet("trust-comment", "", "Set from Sone Web Interface") { core.preferences.trustComment }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ReloadingPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ReloadingPageTest.kt
deleted file mode 100644 (file)
index a9cfd46..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.web.Method.GET
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TemporaryFolder
-import java.nio.file.Files
-import java.nio.file.Paths
-import kotlin.text.Charsets.UTF_8
-
-/**
- * Unit test for [ReloadingPage].
- */
-class ReloadingPageTest : WebPageTest() {
-
-       @Rule @JvmField val tempFolder = TemporaryFolder()
-       private val folder by lazy { tempFolder.newFolder() }
-       private val page by lazy { ReloadingPage<FreenetRequest>("/prefix/", folder.path, "text/plain") }
-
-       @Test
-       fun `page returns correct path prefix`() {
-               assertThat(page.path, equalTo("/prefix/"))
-       }
-
-       @Test
-       fun `page returns that it’s a prefix page`() {
-               assertThat(page.isPrefixPage, equalTo(true))
-       }
-
-       @Test
-       fun `requesting invalid file results in 404`() {
-               request("/prefix/path/file.txt", GET)
-               page.handleRequest(freenetRequest, response)
-               assertThat(response.statusCode, equalTo(404))
-       }
-
-       @Test
-       fun `requesting valid file results in 200 and delivers file`() {
-               Files.write(Paths.get(folder.path, "file.txt"), listOf("Hello", "World"), UTF_8)
-               request("/prefix/path/file.txt", GET)
-               page.handleRequest(freenetRequest, response)
-               assertThat(response.statusCode, equalTo(200))
-               assertThat(response.contentType, equalTo("text/plain"))
-               assertThat(responseBytes, equalTo("Hello\nWorld\n".toByteArray()))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/RescuePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/RescuePageTest.kt
deleted file mode 100644 (file)
index f57d11c..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.core.SoneRescuer
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.GET
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Before
-import org.junit.Test
-import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [RescuePage].
- */
-class RescuePageTest : WebPageTest() {
-
-       private val page = RescuePage(template, webInterface)
-
-       private val soneRescuer = mock<SoneRescuer>()
-
-       override fun getPage() = page
-
-       @Before
-       fun setupSoneRescuer() {
-               whenever(core.getSoneRescuer(currentSone)).thenReturn(soneRescuer)
-       }
-
-       @Test
-       fun `get request sets rescuer in template context`() {
-               request("", GET)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["soneRescuer"], equalTo<Any>(soneRescuer))
-       }
-
-       @Test
-       fun `post request redirects to rescue page`() {
-               request("", POST)
-               verifyRedirect("rescue.html")
-       }
-
-       @Test
-       fun `post request with fetch and invalid edition starts next fetch`() {
-               request("", POST)
-               addHttpRequestParameter("fetch", "true")
-               verifyRedirect("rescue.html") {
-                       verify(soneRescuer, never()).setEdition(anyLong())
-                       verify(soneRescuer).startNextFetch()
-               }
-       }
-
-       @Test
-       fun `post request with fetch and valid edition sets edition and starts next fetch`() {
-               request("", POST)
-               addHttpRequestParameter("fetch", "true")
-               addHttpRequestParameter("edition", "123")
-               verifyRedirect("rescue.html") {
-                       verify(soneRescuer).setEdition(123L)
-                       verify(soneRescuer).startNextFetch()
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/SearchPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/SearchPageTest.kt
deleted file mode 100644 (file)
index 84168f3..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-package net.pterodactylus.sone.web
-
-import com.google.common.base.Optional.absent
-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.Sone
-import net.pterodactylus.sone.test.asOptional
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.junit.Test
-
-/**
- * Unit test for [SearchPage].
- */
-class SearchPageTest : WebPageTest() {
-
-       private val page = SearchPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `empty query redirects to index page`() {
-               verifyRedirect("index.html")
-       }
-
-       @Test
-       fun `empty search phrases redirect to index page`() {
-               addHttpRequestParameter("query", "\"\"")
-               verifyRedirect("index.html")
-       }
-
-       @Test
-       fun `invalid search phrases redirect to index page`() {
-               addHttpRequestParameter("query", "\"")
-               verifyRedirect("index.html")
-       }
-
-       @Test
-       fun `searching for sone link redirects to view sone page`() {
-               addSone("sone-id", mock<Sone>())
-               addHttpRequestParameter("query", "sone://sone-id")
-               verifyRedirect("viewSone.html?sone=sone-id")
-       }
-
-       @Test
-       fun `searching for sone link without prefix redirects to view sone page`() {
-               addSone("sone-id", mock<Sone>())
-               addHttpRequestParameter("query", "sone-id")
-               verifyRedirect("viewSone.html?sone=sone-id")
-       }
-
-       @Test
-       fun `searching for a post link redirects to post page`() {
-               addPost("post-id", mock<Post>())
-               addHttpRequestParameter("query", "post://post-id")
-               verifyRedirect("viewPost.html?post=post-id")
-       }
-
-       @Test
-       fun `searching for a post ID without prefix redirects to post page`() {
-               addPost("post-id", mock<Post>())
-               addHttpRequestParameter("query", "post-id")
-               verifyRedirect("viewPost.html?post=post-id")
-       }
-
-       @Test
-       fun `searching for a reply link redirects to the post page`() {
-               val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
-               addPostReply("reply-id", postReply)
-               addHttpRequestParameter("query", "reply://reply-id")
-               verifyRedirect("viewPost.html?post=post-id")
-       }
-
-       @Test
-       fun `searching for a reply ID redirects to the post page`() {
-               val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
-               addPostReply("reply-id", postReply)
-               addHttpRequestParameter("query", "reply-id")
-               verifyRedirect("viewPost.html?post=post-id")
-       }
-
-       @Test
-       fun `searching for an album link redirects to the image browser`() {
-               addAlbum("album-id", mock<Album>())
-               addHttpRequestParameter("query", "album://album-id")
-               verifyRedirect("imageBrowser.html?album=album-id")
-       }
-
-       @Test
-       fun `searching for an album ID redirects to the image browser`() {
-               addAlbum("album-id", mock<Album>())
-               addHttpRequestParameter("query", "album-id")
-               verifyRedirect("imageBrowser.html?album=album-id")
-       }
-
-       @Test
-       fun `searching for an image link redirects to the image browser`() {
-               addImage("image-id", mock<Image>())
-               addHttpRequestParameter("query", "image://image-id")
-               verifyRedirect("imageBrowser.html?image=image-id")
-       }
-
-       @Test
-       fun `searching for an image ID redirects to the image browser`() {
-               addImage("image-id", mock<Image>())
-               addHttpRequestParameter("query", "image-id")
-               verifyRedirect("imageBrowser.html?image=image-id")
-       }
-
-       private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
-               whenever(this.text).thenReturn(text)
-               postId?.run { whenever(this@apply.postId).thenReturn(postId) }
-               sone?.run { whenever(this@apply.sone).thenReturn(sone) }
-       }
-
-       private fun createPost(id: String, text: String) = mock<Post>().apply {
-               whenever(this.id).thenReturn(id)
-               whenever(recipient).thenReturn(absent())
-               whenever(this.text).thenReturn(text)
-       }
-
-       private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
-               whenever(posts).thenReturn(listOf(post))
-       } ?: mock<Sone>().apply {
-               whenever(posts).thenReturn(listOf(post))
-               whenever(profile).thenReturn(Profile(this))
-       }
-
-       @Test
-       fun `searching for a single word finds the post`() {
-               val postWithMatch = createPost("post-with-match", "the word here")
-               val postWithoutMatch = createPost("post-without-match", "no match here")
-               val soneWithMatch = createSoneWithPost(postWithMatch)
-               val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
-               addSone("sone-with-match", soneWithMatch)
-               addSone("sone-without-match", soneWithoutMatch)
-               addHttpRequestParameter("query", "word")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithMatch))
-       }
-
-       @Test
-       fun `searching for a single word locates word in reply`() {
-               val postWithMatch = createPost("post-with-match", "no match here")
-               val postWithoutMatch = createPost("post-without-match", "no match here")
-               val soneWithMatch = createSoneWithPost(postWithMatch)
-               val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
-               val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
-               val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
-               addPostReply("reply-with-match", replyWithMatch)
-               addPostReply("reply-without-match", replyWithoutMatch)
-               addSone("sone-with-match", soneWithMatch)
-               addSone("sone-without-match", soneWithoutMatch)
-               addHttpRequestParameter("query", "word")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithMatch))
-       }
-
-       private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
-                       createPost("post-$idPostfix", text, recipient).apply {
-                               addSone("sone-$idPostfix", createSoneWithPost(this, sender))
-                       }
-
-       @Test
-       fun `earlier matches score higher than later matches`() {
-               val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
-               val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
-               addHttpRequestParameter("query", "optional ")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
-       }
-
-       @Test
-       fun `searching for required word does not return posts without that word`() {
-               val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
-               createPost("without-required-match", "not a match")
-               addHttpRequestParameter("query", "+required ")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
-       }
-
-       @Test
-       fun `searching for forbidden word does not return posts with that word`() {
-               createSoneWithPost("with-forbidden-match", "forbidden match")
-               val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
-               addHttpRequestParameter("query", "match -forbidden")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
-       }
-
-       @Test
-       fun `searching for a plus sign searches for optional plus sign`() {
-               val postWithMatch = createSoneWithPost("with-match", "with + match")
-               createSoneWithPost("without-match", "without match")
-               addHttpRequestParameter("query", "+")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithMatch))
-       }
-
-       @Test
-       fun `searching for a minus sign searches for optional minus sign`() {
-               val postWithMatch = createSoneWithPost("with-match", "with - match")
-               createSoneWithPost("without-match", "without match")
-               addHttpRequestParameter("query", "-")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithMatch))
-       }
-
-       private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
-               whenever(this.id).thenReturn(id)
-               val recipientId = recipient?.id
-               whenever(this.recipientId).thenReturn(recipientId.asOptional())
-               whenever(this.recipient).thenReturn(recipient.asOptional())
-               whenever(this.text).thenReturn(text)
-       }
-
-       private fun createSone(id: String, firstName: String, middleName: String, lastName: String) = mock<Sone>().apply {
-               whenever(this.id).thenReturn(id)
-               whenever(this.name).thenReturn(id)
-               whenever(this.profile).thenReturn(Profile(this).apply {
-                       this.firstName = firstName
-                       this.middleName = middleName
-                       this.lastName = lastName
-               })
-       }
-
-       @Test
-       fun `searching for a recipient finds the correct post`() {
-               val recipient = createSone("recipient", "reci", "pi", "ent")
-               val postWithMatch = createSoneWithPost("with-match", "test", recipient)
-               createSoneWithPost("without-match", "no match")
-               addHttpRequestParameter("query", "recipient")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["postHits"], contains<Post>(postWithMatch))
-       }
-
-       @Test
-       fun `searching for a field value finds the correct sone`() {
-               val soneWithProfileField = createSone("sone", "s", "o", "ne")
-               soneWithProfileField.profile.addField("field").value = "value"
-               createSoneWithPost("with-match", "test", sender = soneWithProfileField)
-               createSoneWithPost("without-match", "no match")
-               addHttpRequestParameter("query", "value")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(this["soneHits"], contains(soneWithProfileField))
-       }
-
-       @Suppress("UNCHECKED_CAST")
-       private operator fun <T> get(key: String): T? = templateContext[key] as? T
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt
deleted file mode 100644 (file)
index 06d4c19..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.main.SonePlugin
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.sone.web.page.FreenetRequest
-import net.pterodactylus.util.notify.Notification
-import net.pterodactylus.util.template.TemplateContext
-import net.pterodactylus.util.version.Version
-import net.pterodactylus.util.web.Method.GET
-import org.hamcrest.Matcher
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.anyOf
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.containsInAnyOrder
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [SoneTemplatePage].
- */
-class SoneTemplatePageTest : WebPageTest() {
-
-       private val preferences by lazy { core.preferences!! }
-       private val page = object : SoneTemplatePage("path.html", template, webInterface, true) {}
-
-       @Test
-       fun `current sone is retrieved from web interface`() {
-               assertThat(page.getCurrentSone(toadletContext), equalTo(currentSone))
-       }
-
-       @Test
-       fun `retrieving current sone without creation is forwarded to web interface`() {
-               mock<Sone>().let {
-                       whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(it)
-                       assertThat(page.getCurrentSoneWithoutCreatingSession(toadletContext), equalTo(it))
-               }
-       }
-
-       @Test
-       fun `setting the current sone is forwarded to web interface`() {
-               mock<Sone>().let {
-                       page.setCurrentSone(toadletContext, it)
-                       verify(webInterface).setCurrentSone(toadletContext, it)
-               }
-       }
-
-       @Test
-       fun `page title is empty string if no page title key was given`() {
-               SoneTemplatePage("path.html", template, null, webInterface).let { page ->
-                       assertThat(page.getPageTitle(freenetRequest), equalTo(""))
-               }
-       }
-
-       @Test
-       fun `page title is retrieved from l10n if page title key is given`() {
-               SoneTemplatePage("path.html", template, "page.title", webInterface).let { page ->
-                       whenever(l10n.getString("page.title")).thenReturn("Page Title")
-                       assertThat(page.getPageTitle(freenetRequest), equalTo("Page Title"))
-               }
-       }
-
-       @Test
-       fun `additional link nodes contain open search link`() {
-               addHttpRequestHeader("Host", "www.example.com")
-               assertThat(page.getAdditionalLinkNodes(freenetRequest), contains(mapOf(
-                               "rel" to "search",
-                               "type" to "application/opensearchdescription+xml",
-                               "title" to "Sone",
-                               "href" to "http://www.example.com/Sone/OpenSearch.xml"
-               )))
-       }
-
-       @Test
-       fun `style sheets contains sone CSS file`() {
-               assertThat(page.styleSheets, contains("css/sone.css"))
-       }
-
-       @Test
-       fun `shortcut icon is the sone icon`() {
-               assertThat(page.shortcutIcon, equalTo("images/icon.png"))
-       }
-
-       @Test
-       fun `page requires login if require login was specified in the constructor`() {
-               SoneTemplatePage("path.html", template, webInterface, true).let { page ->
-                       assertThat(page.requiresLogin(), equalTo(true))
-               }
-       }
-
-       @Test
-       fun `page does not require login if require login was not specified in the constructor`() {
-               SoneTemplatePage("path.html", template, webInterface, false).let { page ->
-                       assertThat(page.requiresLogin(), equalTo(false))
-               }
-       }
-
-       private fun verifyVariableIsSet(name: String, value: Any) = verifyVariableMatches(name, equalTo<Any>(value))
-
-       private fun <T> verifyVariableMatches(name: String, matcher: Matcher<T>) {
-               page.processTemplate(freenetRequest, templateContext)
-               @Suppress("UNCHECKED_CAST")
-               assertThat(templateContext[name] as T, matcher)
-       }
-
-       @Test
-       fun `preferences are set in template context`() {
-           verifyVariableIsSet("preferences", preferences)
-       }
-
-       @Test
-       fun `current sone is set in template context`() {
-               verifyVariableIsSet("currentSone", currentSone)
-       }
-
-       @Test
-       fun `local sones are set in template context`() {
-               val localSones = listOf(mock<Sone>(), mock<Sone>())
-               whenever(core.localSones).thenReturn(localSones)
-               verifyVariableMatches("localSones", containsInAnyOrder(*localSones.toTypedArray()))
-       }
-
-       @Test
-       fun `freenet request is set in template context`() {
-               verifyVariableIsSet("request", freenetRequest)
-       }
-
-       @Test
-       fun `current version is set in template context`() {
-               verifyVariableIsSet("currentVersion", SonePlugin.getPluginVersion())
-       }
-
-       @Test
-       fun `has latest version is set correctly in template context if true`() {
-               whenever(core.updateChecker.hasLatestVersion()).thenReturn(true)
-               verifyVariableIsSet("hasLatestVersion", true)
-       }
-
-       @Test
-       fun `has latest version is set correctly in template context if false`() {
-               whenever(core.updateChecker.hasLatestVersion()).thenReturn(false)
-               verifyVariableIsSet("hasLatestVersion", false)
-       }
-
-       @Test
-       fun `latest edition is set in template context`() {
-               whenever(core.updateChecker.latestEdition).thenReturn(1234L)
-               verifyVariableIsSet("latestEdition", 1234L)
-       }
-
-       @Test
-       fun `latest version is set in template context`() {
-               whenever(core.updateChecker.latestVersion).thenReturn(Version(1, 2, 3))
-               verifyVariableIsSet("latestVersion", Version(1, 2, 3))
-       }
-
-       @Test
-       fun `latest version time is set in template context`() {
-               whenever(core.updateChecker.latestVersionDate).thenReturn(12345L)
-               verifyVariableIsSet("latestVersionTime", 12345L)
-       }
-
-       private fun createNotification(time: Long) = mock<Notification>().apply {
-               whenever(createdTime).thenReturn(time)
-       }
-
-       @Test
-       fun `notifications are set in template context`() {
-               val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000))
-               whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications)
-               verifyVariableMatches("notifications", contains(notifications[1], notifications[2], notifications[0]))
-       }
-
-       @Test
-       fun `notification hash is set in template context`() {
-               val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000))
-               whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications)
-               verifyVariableIsSet("notificationHash", listOf(notifications[1], notifications[2], notifications[0]).hashCode())
-       }
-
-       @Test
-       fun `handleRequest method is called`() {
-               var called = false
-               val page = object : SoneTemplatePage("path.html", template, webInterface, true) {
-                       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
-                               called = true
-                       }
-               }
-               page.processTemplate(freenetRequest, templateContext)
-               assertThat(called, equalTo(true))
-       }
-
-       @Test
-       fun `redirect does not happen if login is not required`() {
-               val page = SoneTemplatePage("page.html", template, webInterface, false)
-               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
-       }
-
-       @Test
-       fun `redirect does not happen if sone is logged in`() {
-               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
-       }
-
-       @Test
-       fun `redirect does happen if sone is not logged in`() {
-               unsetCurrentSone()
-               request("index.html", GET)
-               assertThat(page.getRedirectTarget(freenetRequest), equalTo("login.html?target=index.html"))
-       }
-
-       @Test
-       fun `redirect does happen with parameters encoded correctly if sone is not logged in`() {
-               unsetCurrentSone()
-               request("index.html", GET)
-               addHttpRequestParameter("foo", "b=r")
-               addHttpRequestParameter("baz", "q&o")
-               assertThat(page.getRedirectTarget(freenetRequest), anyOf(
-                               equalTo("login.html?target=index.html%3Ffoo%3Db%253Dr%26baz%3Dq%2526o"),
-                               equalTo("login.html?target=index.html%3Fbaz%3Dq%2526o%26foo%3Db%253Dr")
-               ))
-       }
-
-       @Test
-       fun `full access requirement is correctly forwarded from the preferences if false`() {
-               assertThat(page.isFullAccessOnly, equalTo(false))
-       }
-
-       @Test
-       fun `full access requirement is correctly forwarded from the preferences if true`() {
-               core.preferences.isRequireFullAccess = true
-               assertThat(page.isFullAccessOnly, equalTo(true))
-       }
-
-       @Test
-       fun `page is disabled if full access is required but request does not have full access`() {
-               core.preferences.isRequireFullAccess = true
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is disabled if login is required but there is no current sone`() {
-               unsetCurrentSone()
-               assertThat(page.isEnabled(toadletContext), equalTo(false))
-       }
-
-       @Test
-       fun `page is enabled if login is required and there is a current sone`() {
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `page is enabled if full access is required and request has full access and login is required and there is a current sone`() {
-               core.preferences.isRequireFullAccess = true
-               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
-               assertThat(page.isEnabled(toadletContext), equalTo(true))
-       }
-
-       @Test
-       fun `page is enabled if no full access is required and login is not required`() {
-               SoneTemplatePage("path.html", template, webInterface, false).let { page ->
-                       assertThat(page.isEnabled(toadletContext), equalTo(true))
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/TrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/TrustPageTest.kt
deleted file mode 100644 (file)
index a29c0c2..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [TrustPage].
- */
-class TrustPageTest : WebPageTest() {
-
-       private val page = TrustPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get method does not redirect`() {
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request with missing sone redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "sone-id")
-               verifyRedirect("return.html") {
-                       verify(core, never()).trustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with existing sone trusts the identity and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "sone-id")
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               verifyRedirect("return.html") {
-                       verify(core).trustSone(eq(currentSone), eq(sone))
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UnbookmarkPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UnbookmarkPageTest.kt
deleted file mode 100644 (file)
index d599f19..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.Mockito.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UnbookmarkPage].
- */
-class UnbookmarkPageTest : WebPageTest() {
-
-       private val page = UnbookmarkPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `get request with all-not-loaded parameter unloads all not loaded posts and redirects to bookmarks`() {
-               addHttpRequestParameter("post", "allNotLoaded")
-               val loadedPost1 = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
-               val loadedPost2 = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
-               val notLoadedPost1 = mock<Post>()
-               val notLoadedPost2 = mock<Post>()
-               whenever(core.bookmarkedPosts).thenReturn(setOf(loadedPost1, loadedPost2, notLoadedPost1, notLoadedPost2))
-               verifyRedirect("bookmarks.html") {
-                       verify(core).unbookmarkPost(notLoadedPost1)
-                       verify(core).unbookmarkPost(notLoadedPost2)
-                       verify(core, never()).unbookmarkPost(loadedPost1)
-                       verify(core, never()).unbookmarkPost(loadedPost2)
-               }
-       }
-
-       @Test
-       fun `post request does not unbookmark not-present post but redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).unbookmarkPost(any())
-               }
-       }
-
-       @Test
-       fun `post request unbookmarks present post and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("returnPage", "return.html")
-               val post = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
-               addPost("post-id", post)
-               verifyRedirect("return.html") {
-                       verify(core).unbookmarkPost(post)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UnfollowSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UnfollowSonePageTest.kt
deleted file mode 100644 (file)
index b9c3433..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UnfollowSonePage].
- */
-class UnfollowSonePageTest : WebPageTest() {
-
-       private val page = UnfollowSonePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request unfollows a single sone and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "sone-id")
-               verifyRedirect("return.html") {
-                       verify(core).unfollowSone(currentSone, "sone-id")
-               }
-       }
-
-       @Test
-       fun `post request unfollows two sones and redirects to return page`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "sone-id1, sone-id2")
-               verifyRedirect("return.html") {
-                       verify(core).unfollowSone(currentSone, "sone-id1")
-                       verify(core).unfollowSone(currentSone, "sone-id2")
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UnlikePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UnlikePageTest.kt
deleted file mode 100644 (file)
index 3bbd1e4..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.util.web.Method
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.ArgumentMatchers
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UnlikePage].
- */
-class UnlikePageTest : WebPageTest() {
-
-       private val page = UnlikePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               page.handleRequest(freenetRequest, templateContext)
-       }
-
-       @Test
-       fun `post request does not remove any likes but redirects`() {
-           request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(currentSone, never()).removeLikedPostId(any())
-                       verify(currentSone, never()).removeLikedReplyId(any())
-               }
-       }
-
-       @Test
-       fun `post request removes post like and redirects`() {
-           request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("type", "post")
-               addHttpRequestParameter("id", "post-id")
-               verifyRedirect("return.html") {
-                       verify(currentSone, never()).removeLikedPostId("post-id")
-                       verify(currentSone, never()).removeLikedReplyId(any())
-               }
-       }
-
-       @Test
-       fun `post request removes reply like and redirects`() {
-           request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("type", "reply")
-               addHttpRequestParameter("id", "reply-id")
-               verifyRedirect("return.html") {
-                       verify(currentSone, never()).removeLikedPostId(any())
-                       verify(currentSone, never()).removeLikedReplyId("reply-id")
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UnlockSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UnlockSonePageTest.kt
deleted file mode 100644 (file)
index 4785156..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UnlockSonePage].
- */
-class UnlockSonePageTest : WebPageTest() {
-
-       private val page = UnlockSonePage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request without sone redirects to return page`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).unlockSone(any())
-               }
-       }
-
-       @Test
-       fun `get request without invalid local sone does not unlock any sone and redirects to return page`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "invalid-sone")
-               verifyRedirect("return.html") {
-                       verify(core, never()).unlockSone(any())
-               }
-       }
-
-       @Test
-       fun `get request without remote sone does not unlock any sone and redirects to return page`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "remote-sone")
-               addSone("remote-sone", mock<Sone>())
-               verifyRedirect("return.html") {
-                       verify(core, never()).unlockSone(any())
-               }
-       }
-
-       @Test
-       fun `get request with local sone unlocks sone and redirects to return page`() {
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "local-sone")
-               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
-               addLocalSone("local-sone", sone)
-               verifyRedirect("return.html") {
-                       verify(core).unlockSone(sone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UntrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UntrustPageTest.kt
deleted file mode 100644 (file)
index 166fd34..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.util.web.Method.POST
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UntrustPage].
- */
-class UntrustPageTest : WebPageTest() {
-
-       private val page = UntrustPage(template, webInterface)
-
-       override fun getPage() = page
-
-       @Test
-       fun `get request does not redirect`() {
-               page.handleRequest(freenetRequest, templateContext)
-               verify(core, never()).untrustSone(eq(currentSone), any())
-       }
-
-       @Test
-       fun `post request without sone parameter does not untrust but redirects`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               verifyRedirect("return.html") {
-                       verify(core, never()).untrustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with invalid sone parameter does not untrust but redirects`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "no-sone")
-               verifyRedirect("return.html") {
-                       verify(core, never()).untrustSone(eq(currentSone), any())
-               }
-       }
-
-       @Test
-       fun `post request with valid sone parameter untrusts and redirects`() {
-               request("", POST)
-               addHttpRequestParameter("returnPage", "return.html")
-               addHttpRequestParameter("sone", "sone-id")
-               val sone = mock<Sone>()
-               addSone("sone-id", sone)
-               verifyRedirect("return.html") {
-                       verify(core).untrustSone(currentSone, sone)
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/UploadImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/UploadImagePageTest.kt
deleted file mode 100644 (file)
index 0eb34d6..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Album
-import net.pterodactylus.sone.data.Image
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.data.TemporaryImage
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.mockBuilder
-import net.pterodactylus.sone.test.whenever
-import net.pterodactylus.util.web.Method.POST
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.junit.Test
-import org.mockito.Mockito.any
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-/**
- * Unit test for [UploadImagePage].
- */
-class UploadImagePageTest : WebPageTest() {
-
-       private val parentAlbum = mock<Album>().apply {
-               whenever(id).thenReturn("parent-id")
-               whenever(sone).thenReturn(currentSone)
-       }
-
-       override fun getPage() = UploadImagePage(template, webInterface)
-
-       @Test
-       fun `get request does not redirect or upload anything`() {
-               page.handleRequest(freenetRequest, templateContext)
-               verify(core, never()).createTemporaryImage(any(), any())
-               verify(core, never()).createImage(any(), any(), any())
-       }
-
-       @Test
-       fun `post request without parent results in no permission error page`() {
-               request("", POST)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with parent that is not the current sone results in no permission error page`() {
-               request("", POST)
-               addHttpRequestParameter("parent", "parent-id")
-               whenever(parentAlbum.sone).thenReturn(mock<Sone>())
-               addAlbum("parent-id", parentAlbum)
-               verifyRedirect("noPermission.html")
-       }
-
-       @Test
-       fun `post request with empty name redirects to error page`() {
-               request("", POST)
-               addAlbum("parent-id", parentAlbum)
-               addHttpRequestParameter("parent", "parent-id")
-               addHttpRequestParameter("title", " ")
-               verifyRedirect("emptyImageTitle.html")
-       }
-
-       @Test
-       fun `uploading an invalid image results in no redirect and message set in template context`() {
-               request("", POST)
-               addAlbum("parent-id", parentAlbum)
-               addHttpRequestParameter("parent", "parent-id")
-               addHttpRequestParameter("title", "title")
-               addUploadedFile("image", "image.png", "image/png", "no-image.png")
-               page.handleRequest(freenetRequest, templateContext)
-               verify(core, never()).createTemporaryImage(any(), any())
-               assertThat(templateContext["messages"] as String?, equalTo<String>("Page.UploadImage.Error.InvalidImage"))
-       }
-
-       @Test
-       fun `uploading a valid image uploads image and redirects to album browser`() {
-               request("", POST)
-               addAlbum("parent-id", parentAlbum)
-               addHttpRequestParameter("parent", "parent-id")
-               addHttpRequestParameter("title", "Title")
-               addHttpRequestParameter("description", "Description")
-               addUploadedFile("image", "image.png", "image/png", "image.png")
-               val temporaryImage = TemporaryImage("temp-image")
-               val imageModifier = mockBuilder<Image.Modifier>()
-               val image = mock<Image>().apply {
-                       whenever(modify()).thenReturn(imageModifier)
-               }
-               whenever(core.createTemporaryImage(eq("image/png"), any())).thenReturn(temporaryImage)
-               whenever(core.createImage(currentSone, parentAlbum, temporaryImage)).thenReturn(image)
-               verifyRedirect("imageBrowser.html?album=parent-id") {
-                       verify(image).modify()
-                       verify(imageModifier).setWidth(2)
-                       verify(imageModifier).setHeight(1)
-                       verify(imageModifier).setTitle("Title")
-                       verify(imageModifier).setDescription("Description")
-                       verify(imageModifier).update()
-               }
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ViewPostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ViewPostPageTest.kt
deleted file mode 100644 (file)
index 36c70d2..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Test
-
-/**
- * Unit test for [ViewPostPage].
- */
-class ViewPostPageTest : WebPageTest() {
-
-       private val page = ViewPostPage(template, webInterface)
-       private val post = mock<Post>()
-
-       override fun getPage() = page
-
-       @Test
-       fun `the view post page is link-excepted`() {
-               assertThat(page.isLinkExcepted(null), equalTo(true))
-       }
-
-       @Test
-       fun `get request without parameters stores null in template context`() {
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["post"], nullValue())
-               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
-       }
-
-       @Test
-       fun `get request with invalid post id stores null in template context`() {
-               addHttpRequestParameter("post", "invalid-post-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["post"], nullValue())
-               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
-       }
-
-       @Test
-       fun `get request with valid post id stores post in template context`() {
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["post"], equalTo<Any>(post))
-               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
-       }
-
-       @Test
-       fun `get request with valid post id and raw=true stores post in template context`() {
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               addHttpRequestParameter("raw", "true")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["post"], equalTo<Any>(post))
-               assertThat(templateContext["raw"] as? Boolean, equalTo(true))
-       }
-
-       @Test
-       fun `page title for request without parameters is default title`() {
-               assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewPost.Title"))
-       }
-
-       @Test
-       fun `page title for request with invalid post is default title`() {
-               addHttpRequestParameter("post", "invalid-post-id")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewPost.Title"))
-       }
-
-       @Test
-       fun `page title for request with valid post is first twenty chars of post plus sone name plus default title`() {
-               whenever(currentSone.profile).thenReturn(Profile(currentSone).apply {
-                       firstName = "First"
-                       middleName = "M."
-                       lastName = "Last"
-               })
-               whenever(post.sone).thenReturn(currentSone)
-               whenever(post.text).thenReturn("This is a text that is longer than twenty characters.")
-               addPost("post-id", post)
-               addHttpRequestParameter("post", "post-id")
-               assertThat(page.getPageTitle(freenetRequest), equalTo("This is a text that … - First M. Last - Page.ViewPost.Title"))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ViewSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ViewSonePageTest.kt
deleted file mode 100644 (file)
index 428da7b..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-package net.pterodactylus.sone.web
-
-import net.pterodactylus.sone.data.Post
-import net.pterodactylus.sone.data.PostReply
-import net.pterodactylus.sone.data.Profile
-import net.pterodactylus.sone.data.Sone
-import net.pterodactylus.sone.test.asOptional
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.test.whenever
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.contains
-import org.hamcrest.Matchers.equalTo
-import org.hamcrest.Matchers.nullValue
-import org.junit.Before
-import org.junit.Test
-
-/**
- * Unit test for [ViewSonePage].
- */
-class ViewSonePageTest : WebPageTest() {
-
-       init {
-               whenever(currentSone.id).thenReturn("sone-id")
-       }
-
-       private val page = ViewSonePage(template, webInterface)
-       private val post1 = createPost("post1", "First Post.", 1000, currentSone)
-       private val post2 = createPost("post2", "Second Post.", 2000, currentSone)
-       private val foreignPost1 = createPost("foreign-post1", "First Foreign Post.", 1000, mock<Sone>())
-       private val foreignPost2 = createPost("foreign-post2", "Second Foreign Post.", 2000, mock<Sone>())
-       private val directed1 = createPost("post3", "First directed.", 1500, mock<Sone>(), recipient = currentSone)
-       private val directed2 = createPost("post4", "Second directed.", 2500, mock<Sone>(), recipient = currentSone)
-
-       override fun getPage() = page
-
-       @Before
-       fun setup() {
-               whenever(currentSone.posts).thenReturn(mutableListOf(post2, post1))
-               whenever(core.getDirectedPosts("sone-id")).thenReturn(setOf(directed1, directed2))
-               core.preferences.postsPerPage = 2
-       }
-
-       @Test
-       fun `get request without sone parameter stores null in template context`() {
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["sone"], nullValue())
-               assertThat(templateContext["soneId"], equalTo<Any>(""))
-       }
-
-       @Test
-       fun `get request with invalid sone parameter stores null in template context`() {
-               addHttpRequestParameter("sone", "invalid-sone-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["sone"], nullValue())
-               assertThat(templateContext["soneId"], equalTo<Any>("invalid-sone-id"))
-       }
-
-       @Test
-       fun `get request with valid sone parameter stores sone in template context`() {
-               whenever(currentSone.posts).thenReturn(mutableListOf())
-               whenever(core.getDirectedPosts("sone-id")).thenReturn(emptyList())
-               addHttpRequestParameter("sone", "sone-id")
-               addSone("sone-id", currentSone)
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["sone"], equalTo<Any>(currentSone))
-               assertThat(templateContext["soneId"], equalTo<Any>("sone-id"))
-       }
-
-       private fun createPost(id: String, text: String, time: Long, sender: Sone? = null, recipient: Sone? = null) = mock<Post>().apply {
-               whenever(this.id).thenReturn(id)
-               sender?.run { whenever(this@apply.sone).thenReturn(this) }
-               val recipientId = recipient?.id
-               whenever(this.recipientId).thenReturn(recipientId.asOptional())
-               whenever(this.recipient).thenReturn(recipient.asOptional())
-               whenever(this.time).thenReturn(time)
-               whenever(this.text).thenReturn(text)
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `request with valid sone stores posts and directed posts in template context`() {
-               addSone("sone-id", currentSone)
-               addHttpRequestParameter("sone", "sone-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["posts"] as Iterable<Post>, contains(directed2, post2))
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `second page of posts is shown correctly`() {
-               addSone("sone-id", currentSone)
-               addHttpRequestParameter("sone", "sone-id")
-               addHttpRequestParameter("postPage", "1")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["posts"] as Iterable<Post>, contains(directed1, post1))
-       }
-
-       private fun createReply(text: String, time: Long, post: Post?) = mock<PostReply>().apply {
-               whenever(this.text).thenReturn(text)
-               whenever(this.time).thenReturn(time)
-               whenever(this.post).thenReturn(post.asOptional())
-       }
-
-       @Test
-       @Suppress("UNCHECKED_CAST")
-       fun `replies are shown correctly`() {
-           val reply1 = createReply("First Reply", 1500, foreignPost1)
-               val reply2 = createReply("Second Reply", 2500, foreignPost2)
-               val reply3 = createReply("Third Reply", 1750, post1)
-               val reply4 = createReply("Fourth Reply", 2250, post2)
-           val reply5 = createReply("Fifth Reply", 1600, post1)
-           val reply6 = createReply("Sixth Reply", 2100, directed1)
-           val reply7 = createReply("Seventh Reply", 2200, null)
-           val reply8 = createReply("Eigth Reply", 2300, foreignPost1)
-               whenever(currentSone.replies).thenReturn(setOf(reply1, reply2, reply3, reply4, reply5, reply6, reply7, reply8))
-               whenever(core.getReplies("post1")).thenReturn(listOf(reply3, reply5))
-               whenever(core.getReplies("post2")).thenReturn(listOf(reply4))
-               whenever(core.getReplies("foreign-post1")).thenReturn(listOf(reply1))
-               whenever(core.getReplies("foreign-post2")).thenReturn(listOf(reply2))
-               whenever(core.getReplies("post3")).thenReturn(listOf(reply6))
-               addSone("sone-id", currentSone)
-               addHttpRequestParameter("sone", "sone-id")
-               page.handleRequest(freenetRequest, templateContext)
-               assertThat(templateContext["repliedPosts"] as Iterable<Post>, contains(foreignPost2,  foreignPost1))
-       }
-
-       @Test
-       fun `page title is default for request without parameters`() {
-           assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewSone.Page.TitleWithoutSone"))
-       }
-
-       @Test
-       fun `page title is default for request with invalid sone parameters`() {
-               addHttpRequestParameter("sone", "invalid-sone-id")
-           assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewSone.Page.TitleWithoutSone"))
-       }
-
-       @Test
-       fun `page title contains sone name for request with sone parameters`() {
-               addHttpRequestParameter("sone", "sone-id")
-               addSone("sone-id", currentSone)
-               whenever(currentSone.profile).thenReturn(Profile(currentSone).apply {
-                       firstName = "First"
-                       middleName = "M."
-                       lastName = "Last"
-               })
-           assertThat(page.getPageTitle(freenetRequest), equalTo("First M. Last - Page.ViewSone.Title"))
-       }
-
-       @Test
-       fun `page is link-excepted`() {
-           assertThat(page.isLinkExcepted(null), equalTo(true))
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/AboutPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/AboutPageTest.kt
new file mode 100644 (file)
index 0000000..93672a1
--- /dev/null
@@ -0,0 +1,50 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.main.SonePlugin.PluginHomepage
+import net.pterodactylus.sone.main.SonePlugin.PluginVersion
+import net.pterodactylus.sone.main.SonePlugin.PluginYear
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.AboutPage
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [AboutPage].
+ */
+class AboutPageTest: WebPageTest() {
+
+       private val version = "0.1.2"
+       private val year = 1234
+       private val homepage = "home://page"
+       private val page = AboutPage(template, webInterface, PluginVersion(version), PluginYear(year), PluginHomepage(homepage))
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("about.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page sets correct version in template context`() {
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["version"], equalTo<Any>(version))
+       }
+
+       @Test
+       fun `page sets correct homepage in template context`() {
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["homepage"], equalTo<Any>(homepage))
+       }
+
+       @Test
+       fun `page sets correct year in template context`() {
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["year"], equalTo<Any>(year))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarkPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarkPageTest.kt
new file mode 100644 (file)
index 0000000..b5d10cc
--- /dev/null
@@ -0,0 +1,58 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.BookmarkPage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [BookmarkPage].
+ */
+class BookmarkPageTest : WebPageTest() {
+
+       private val page = BookmarkPage(template, webInterface)
+       override fun getPage() = page
+
+       @Test
+       fun `path is set correctly`() {
+               assertThat(page.path, equalTo("bookmark.html"))
+       }
+
+       @Test
+       fun `get request does not bookmark anything and does not redirect`() {
+               page.processTemplate(freenetRequest, templateContext)
+               verify(core, never()).bookmarkPost(any())
+       }
+
+       private fun setupBookmarkRequest() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return-page.html")
+               addHttpRequestParameter("post", "post-id")
+       }
+
+       @Test
+       fun `post is bookmarked correctly`() {
+               setupBookmarkRequest()
+               val post = mock<Post>()
+               addPost("post-id", post)
+               verifyRedirect("return-page.html") {
+                       verify(core).bookmarkPost(post)
+               }
+       }
+
+       @Test
+       fun `non-existing post is not bookmarked`() {
+               setupBookmarkRequest()
+               verifyRedirect("return-page.html") {
+                       verify(core, never()).bookmarkPost(any())
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarksPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/BookmarksPageTest.kt
new file mode 100644 (file)
index 0000000..6b4bbae
--- /dev/null
@@ -0,0 +1,60 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.BookmarksPage
+import net.pterodactylus.util.collection.Pagination
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [BookmarksPage].
+ */
+class BookmarksPageTest: WebPageTest() {
+
+       private val page = BookmarksPage(template, webInterface)
+       private val post1 = createLoadedPost(1000)
+       private val post2 = createLoadedPost(3000)
+       private val post3 = createLoadedPost(2000)
+
+       private fun createLoadedPost(time: Long) = mock<Post>().apply {
+               whenever(isLoaded).thenReturn(true)
+               whenever(this.time).thenReturn(time)
+       }
+
+       @Before
+       fun setupBookmarkedPostsAndPagination() {
+               whenever(core.bookmarkedPosts).thenReturn(setOf(post1, post2, post3))
+               core.preferences.postsPerPage = 5
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("bookmarks.html"))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `page sets correct posts in template context`() {
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["posts"] as Collection<Post>, contains(post2, post3, post1))
+               assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(post2, post3, post1))
+               assertThat(templateContext["postsNotLoaded"], equalTo<Any>(false))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `page does not put unloaded posts in template context but sets a flag`() {
+               whenever(post3.isLoaded).thenReturn(false)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["posts"] as Collection<Post>, contains(post2, post1))
+               assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(post2, post1))
+               assertThat(templateContext["postsNotLoaded"], equalTo<Any>(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateAlbumPageTest.kt
new file mode 100644 (file)
index 0000000..c4f2ee4
--- /dev/null
@@ -0,0 +1,105 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Album.Modifier
+import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.selfMock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.CreateAlbumPage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [CreateAlbumPage].
+ */
+class CreateAlbumPageTest: WebPageTest() {
+
+       private val page = CreateAlbumPage(template, webInterface)
+
+       override fun getPage() = page
+
+       private val parentAlbum = createAlbum("parent-id")
+       private val newAlbum = createAlbum("album-id")
+
+       @Before
+       fun setupAlbums() {
+               whenever(core.createAlbum(currentSone, parentAlbum)).thenReturn(newAlbum)
+               whenever(currentSone.rootAlbum).thenReturn(parentAlbum)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("createAlbum.html"))
+       }
+
+       @Test
+       fun `get request shows template`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `missing name results in attribute being set in template context`() {
+               request("", POST)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["nameMissing"], equalTo<Any>(true))
+       }
+
+       private fun createAlbum(albumId: String) = deepMock<Album>().apply {
+               whenever(id).thenReturn(albumId)
+               selfMock<Modifier>().let { modifier ->
+                       whenever(modifier.update()).thenReturn(this@apply)
+                       whenever(this@apply.modify()).thenReturn(modifier)
+               }
+       }
+
+       @Test
+       fun `title and description are set correctly on the album`() {
+               request("", POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestParameter("name", "new name")
+               addHttpRequestParameter("description", "new description")
+               addHttpRequestParameter("parent", "parent-id")
+               verifyRedirect("imageBrowser.html?album=album-id") {
+                       verify(newAlbum).modify()
+                       verify(newAlbum.modify()).setTitle("new name")
+                       verify(newAlbum.modify()).setDescription("new description")
+                       verify(newAlbum.modify()).update()
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `root album is used if no parent is specified`() {
+               request("", POST)
+               addHttpRequestParameter("name", "new name")
+               addHttpRequestParameter("description", "new description")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `empty album title redirects to error page`() {
+               request("", POST)
+               whenever(newAlbum.modify().update()).thenThrow(AlbumTitleMustNotBeEmpty::class.java)
+               addHttpRequestParameter("name", "new name")
+               addHttpRequestParameter("description", "new description")
+               verifyRedirect("emptyAlbumTitle.html")
+       }
+
+       @Test
+       fun `album description is filtered`() {
+               request("", POST)
+               addHttpRequestParameter("name", "new name")
+               addHttpRequestParameter("description", "new http://localhost:12345/KSK@foo description")
+               addHttpRequestHeader("Host", "localhost:12345")
+               verifyRedirect("imageBrowser.html?album=album-id") {
+                       verify(newAlbum.modify()).setDescription("new KSK@foo description")
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreatePostPageTest.kt
new file mode 100644 (file)
index 0000000..45a2451
--- /dev/null
@@ -0,0 +1,97 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Optional.absent
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.asOptional
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.CreatePostPage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [CreatePostPage].
+ */
+class CreatePostPageTest: WebPageTest() {
+
+       private val page = CreatePostPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("createPost.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `return page is set in template context`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+       @Test
+       fun `post is created correctly`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("text", "post text")
+               verifyRedirect("return.html") {
+                       verify(core).createPost(currentSone, absent(), "post text")
+               }
+       }
+
+       @Test
+       fun `creating an empty post is denied`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("text", "  ")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["errorTextEmpty"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `a sender can be selected`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("text", "post text")
+               addHttpRequestParameter("sender", "sender-id")
+               val sender = mock<Sone>()
+               addLocalSone("sender-id", sender)
+               verifyRedirect("return.html") {
+                       verify(core).createPost(sender, absent(), "post text")
+               }
+       }
+
+       @Test
+       fun `a recipient can be selected`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("text", "post text")
+               addHttpRequestParameter("recipient", "recipient-id")
+               val recipient = mock<Sone>()
+               addSone("recipient-id", recipient)
+               verifyRedirect("return.html") {
+                       verify(core).createPost(currentSone, recipient.asOptional(), "post text")
+               }
+       }
+
+       @Test
+       fun `text is filtered correctly`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("text", "post http://localhost:12345/KSK@foo text")
+               addHttpRequestHeader("Host", "localhost:12345")
+               verifyRedirect("return.html") {
+                       verify(core).createPost(currentSone, absent(), "post KSK@foo text")
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateReplyPageTest.kt
new file mode 100644 (file)
index 0000000..052620a
--- /dev/null
@@ -0,0 +1,104 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.CreateReplyPage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [CreateReplyPage].
+ */
+class CreateReplyPageTest: WebPageTest() {
+
+       private val page = CreateReplyPage(template, webInterface)
+       override fun getPage() = page
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("createReply.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `reply is created correctly`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "new text")
+               val post = mock<Post>().apply { addPost("post-id", this) }
+               verifyRedirect("return.html") {
+                       verify(core).createReply(currentSone, post, "new text")
+               }
+       }
+
+       @Test
+       fun `reply is filtered`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "new http://localhost:12345/KSK@foo text")
+               addHttpRequestHeader("Host", "localhost:12345")
+               val post = mock<Post>().apply { addPost("post-id", this) }
+               verifyRedirect("return.html") {
+                       verify(core).createReply(currentSone, post, "new KSK@foo text")
+               }
+       }
+
+       @Test
+       fun `reply is created with correct sender`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "new text")
+               addHttpRequestParameter("sender", "sender-id")
+               val sender = mock<Sone>().apply { addLocalSone("sender-id", this) }
+               val post = mock<Post>().apply { addPost("post-id", this) }
+               verifyRedirect("return.html") {
+                       verify(core).createReply(sender, post, "new text")
+               }
+       }
+
+       @Test
+       fun `empty text sets parameters in template contexty`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "  ")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["errorTextEmpty"], equalTo<Any>(true))
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+               assertThat(templateContext["postId"], equalTo<Any>("post-id"))
+               assertThat(templateContext["text"], equalTo<Any>(""))
+       }
+
+       @Test
+       fun `user is redirected to no permissions page if post does not exist`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "new text")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `get request stores parameters in template context`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("text", "new text")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+               assertThat(templateContext["postId"], equalTo<Any>("post-id"))
+               assertThat(templateContext["text"], equalTo<Any>("new text"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/CreateSonePageTest.kt
new file mode 100644 (file)
index 0000000..f14d536
--- /dev/null
@@ -0,0 +1,150 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.CreateSonePage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [CreateSonePage].
+ */
+class CreateSonePageTest: WebPageTest() {
+
+       private val page = CreateSonePage(template, webInterface)
+       override fun getPage() = page
+
+       private val localSones_ = listOf(
+                       createSone("local-sone1"),
+                       createSone("local-sone2"),
+                       createSone("local-sone3")
+       )
+
+       private fun createSone(id: String) = mock<Sone>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(profile).thenReturn(Profile(this))
+       }
+
+       private val ownIdentities_ = listOf(
+                       createOwnIdentity("own-id-1", "Sone"),
+                       createOwnIdentity("own-id-2", "Test", "Foo"),
+                       createOwnIdentity("own-id-3"),
+                       createOwnIdentity("own-id-4", "Sone")
+       )
+
+       private fun createOwnIdentity(id: String, vararg contexts: String) = mock<OwnIdentity>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.nickname).thenReturn(id)
+               whenever(this.contexts).thenReturn(contexts.toSet())
+               whenever(this.hasContext(anyString())).thenAnswer { invocation -> invocation.getArgument<String>(0) in contexts }
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("createSone.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       private fun addExistingSones() {
+               listOf(2, 0, 1).map { localSones_[it] }.forEach { addLocalSone(it.id, it) }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `get request stores sorted list of local sones in template context`() {
+               addExistingSones()
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["sones"] as Collection<Sone>, contains(localSones_[0], localSones_[1], localSones_[2]))
+       }
+
+       private fun addExistingOwnIdentities() {
+               listOf(2, 0, 3, 1).map { ownIdentities_[it] }.forEach { addOwnIdentity(it) }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `get request stores sorted sones without sone context in the template context`() {
+               addExistingOwnIdentities()
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["identitiesWithoutSone"] as Collection<OwnIdentity>, contains(ownIdentities_[1], ownIdentities_[2]))
+       }
+
+       @Test
+       fun `sone is created and logged in`() {
+               addExistingOwnIdentities()
+               request("", POST)
+               addHttpRequestParameter("identity", "own-id-3")
+               val newSone = mock<Sone>()
+               whenever(core.createSone(ownIdentities_[2])).thenReturn(newSone)
+               verifyRedirect("index.html") {
+                       verify(webInterface).setCurrentSone(toadletContext, newSone)
+               }
+       }
+
+       @Test
+       fun `on invalid identity id a flag is set in the template context`() {
+               request("", POST)
+               addHttpRequestParameter("identity", "own-id-3")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["errorNoIdentity"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `if sone is not created user is still redirected to index`() {
+               addExistingOwnIdentities()
+               request("", POST)
+               addHttpRequestParameter("identity", "own-id-3")
+               whenever(core.createSone(ownIdentities_[2])).thenReturn(null)
+               verifyRedirect("index.html") {
+                       verify(core).createSone(ownIdentities_[2])
+                       verify(webInterface).setCurrentSone(toadletContext, null)
+               }
+       }
+
+       @Test
+       fun `create sone is not shown in menu if full access is required but client doesn’t have full access`() {
+               core.preferences.isRequireFullAccess = true
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `create sone is shown in menu if no sone is logged in`() {
+               unsetCurrentSone()
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `create sone is shown in menu if a single sone exists`() {
+               addLocalSone("local-sone", localSones_[0])
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `create sone is not shown in menu if more than one sone exists`() {
+               addLocalSone("local-sone1", localSones_[0])
+               addLocalSone("local-sone2", localSones_[1])
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `create sone is shown in menu if no sone is logged in and client has full access`() {
+               core.preferences.isRequireFullAccess = true
+               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
+               unsetCurrentSone()
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteAlbumPageTest.kt
new file mode 100644 (file)
index 0000000..3efd109
--- /dev/null
@@ -0,0 +1,116 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeleteAlbumPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteAlbumPage].
+ */
+class DeleteAlbumPageTest: WebPageTest() {
+
+       private val page = DeleteAlbumPage(template, webInterface)
+
+       private val sone = mock<Sone>()
+       private val album = mock<Album>()
+       private val parentAlbum = mock<Album>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupAlbums() {
+               whenever(sone.id).thenReturn("sone-id")
+               whenever(sone.isLocal).thenReturn(true)
+               whenever(parentAlbum.id).thenReturn("parent-id")
+               whenever(parentAlbum.isRoot).thenReturn(true)
+               whenever(album.id).thenReturn("album-id")
+               whenever(album.sone).thenReturn(sone)
+               whenever(album.parent).thenReturn(parentAlbum)
+               whenever(sone.rootAlbum).thenReturn(parentAlbum)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("deleteAlbum.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `get request with invalid album ID results in redirect to invalid page`() {
+               request("", GET)
+               whenever(core.getAlbum(anyString())).thenReturn(null)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid album ID sets album in template context`() {
+               request("", GET)
+               val album = mock<Album>()
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["album"], equalTo<Any>(album))
+       }
+
+       @Test
+       fun `post request redirects to invalid page if album is invalid`() {
+               request("", POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request redirects to no permissions page if album is not local`() {
+               request("", POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with abort delete parameter set redirects to album browser`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("abortDelete", "true")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `album is deleted and page redirects to sone if parent album is root album`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               verifyRedirect("imageBrowser.html?sone=sone-id") {
+                       verify(core).deleteAlbum(album)
+               }
+       }
+
+       @Test
+       fun `album is deleted and page redirects to album if parent album is not root album`() {
+               request("", POST)
+               whenever(parentAlbum.isRoot).thenReturn(false)
+               whenever(sone.rootAlbum).thenReturn(mock<Album>())
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               verifyRedirect("imageBrowser.html?album=parent-id") {
+                       verify(core).deleteAlbum(album)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteImagePageTest.kt
new file mode 100644 (file)
index 0000000..78000a1
--- /dev/null
@@ -0,0 +1,92 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeleteImagePage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteImagePage].
+ */
+class DeleteImagePageTest: WebPageTest() {
+
+       private val page = DeleteImagePage(template, webInterface)
+       private val image = mock<Image>()
+       private val sone = mock<Sone>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupImage() {
+               val album = mock<Album>()
+               whenever(album.id).thenReturn("album-id")
+               whenever(image.id).thenReturn("image-id")
+               whenever(image.sone).thenReturn(sone)
+               whenever(image.album).thenReturn(album)
+               whenever(sone.isLocal).thenReturn(true)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("deleteImage.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `get request with invalid image redirects to invalid page`() {
+               request("", GET)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with image from non-local sone redirects to no permissions page`() {
+               request("", GET)
+               whenever(sone.isLocal).thenReturn(false)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `get request with image from local sone sets image in template context`() {
+               request("", GET)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["image"], equalTo<Any>(image))
+       }
+
+       @Test
+       fun `post request with abort delete flag set redirects to image browser`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("abortDelete", "true")
+               verifyRedirect("imageBrowser.html?image=image-id")
+       }
+
+       @Test
+       fun `post request deletes image and redirects to image browser`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               verifyRedirect("imageBrowser.html?album=album-id") {
+                       verify(webInterface.core).deleteImage(image)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeletePostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeletePostPageTest.kt
new file mode 100644 (file)
index 0000000..152e70d
--- /dev/null
@@ -0,0 +1,114 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeletePostPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeletePostPage].
+ */
+class DeletePostPageTest : WebPageTest() {
+
+       private val page = DeletePostPage(template, webInterface)
+
+       private val post = mock<Post>()
+       private val sone = mock<Sone>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupPost() {
+               whenever(post.sone).thenReturn(sone)
+               whenever(sone.isLocal).thenReturn(true)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("deletePost.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `get request with invalid post redirects to no permission page`() {
+               request("", GET)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `get request with valid post sets post and return page in template context`() {
+               request("", GET)
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["post"], equalTo<Any>(post))
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+       @Test
+       fun `post request with invalid post redirects to no permission page`() {
+               request("", POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with post from non-local sone redirects to no permission page`() {
+               request("", POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with confirmation deletes post and redirects to return page`() {
+               request("", POST)
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("confirmDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core).deletePost(post)
+               }
+       }
+
+       @Test
+       fun `post request with abort delete does not delete post and redirects to return page`() {
+               request("", POST)
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("abortDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core, never()).deletePost(post)
+               }
+       }
+
+       @Test
+       fun `post request without delete or abort sets post in template context`() {
+               request("", POST)
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["post"], equalTo<Any>(post))
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteProfileFieldPageTest.kt
new file mode 100644 (file)
index 0000000..ebfadcc
--- /dev/null
@@ -0,0 +1,87 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteProfileFieldPage].
+ */
+class DeleteProfileFieldPageTest: WebPageTest() {
+
+       private val page = DeleteProfileFieldPage(template, webInterface)
+
+       private val profile = Profile(currentSone)
+       private val field = profile.addField("name")
+
+       override fun getPage() = page
+
+       @Before
+       fun setupProfile() {
+               whenever(currentSone.profile).thenReturn(profile)
+               field.value = "value"
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("deleteProfileField.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `get request with invalid field name redirects to invalid page`() {
+               request("", GET)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with invalid field name redirects to invalid page`() {
+               request("", POST)
+               addHttpRequestParameter("field", "wrong-id")
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid field name sets field in template context`() {
+               request("", GET)
+               addHttpRequestParameter("field", field.id)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["field"], equalTo<Any>(field))
+       }
+
+       @Test
+       fun `post request without confirm redirects to edit profile page`() {
+               request("", POST)
+               addHttpRequestParameter("field", field.id)
+               verifyRedirect("editProfile.html#profile-fields") {
+                       verify(currentSone, never()).profile = any()
+               }
+       }
+
+       @Test
+       fun `post request with confirm removes field and redirects to edit profile page`() {
+               request("", POST)
+               addHttpRequestParameter("field", field.id)
+               addHttpRequestParameter("confirm", "true")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(profile.getFieldById(field.id), nullValue())
+                       verify(currentSone).profile = profile
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteReplyPageTest.kt
new file mode 100644 (file)
index 0000000..ff65e9d
--- /dev/null
@@ -0,0 +1,106 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeleteReplyPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteReplyPage].
+ */
+class DeleteReplyPageTest : WebPageTest() {
+
+       private val page = DeleteReplyPage(template, webInterface)
+
+       private val sone = mock<Sone>()
+       private val reply = mock<PostReply>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupReply() {
+               whenever(sone.isLocal).thenReturn(true)
+               whenever(reply.sone).thenReturn(sone)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("deleteReply.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `get request sets reply ID and return page in template context`() {
+               request("", GET)
+               addHttpRequestParameter("reply", "reply-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["reply"], equalTo<Any>("reply-id"))
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+       @Test
+       fun `post request without any action sets reply ID and return page in template context`() {
+               request("", POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestParameter("reply", "reply-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["reply"], equalTo<Any>("reply-id"))
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+       @Test
+       fun `trying to delete a reply with an invalid ID results in no permission page`() {
+               request("", POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `trying to delete a reply from a non-local sone results in no permission page`() {
+               request("", POST)
+               addHttpRequestParameter("reply", "reply-id")
+               whenever(sone.isLocal).thenReturn(false)
+               addPostReply("reply-id", reply)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `confirming deletion of reply deletes the reply and redirects to return page`() {
+               request("", POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestParameter("reply", "reply-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("confirmDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core).deleteReply(reply)
+               }
+       }
+
+       @Test
+       fun `aborting deletion of reply redirects to return page`() {
+               request("", POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestParameter("reply", "reply-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("abortDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core, never()).deleteReply(reply)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DeleteSonePageTest.kt
new file mode 100644 (file)
index 0000000..b94c3fa
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DeleteSonePage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteSonePage].
+ */
+class DeleteSonePageTest : WebPageTest() {
+
+       private val page = DeleteSonePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("deleteSone.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           whenever(l10n.getString("Page.DeleteSone.Title")).thenReturn("delete sone page")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("delete sone page"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request without delete confirmation redirects to index`() {
+               request("", POST)
+               verifyRedirect("index.html") {
+                       verify(core, never()).deleteSone(any())
+               }
+       }
+
+       @Test
+       fun `post request with delete confirmation deletes sone and redirects to index`() {
+               request("", POST)
+               addHttpRequestParameter("deleteSone", "true")
+               verifyRedirect("index.html") {
+                       verify(core).deleteSone(currentSone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DismissNotificationPageTest.kt
new file mode 100644 (file)
index 0000000..b6524a8
--- /dev/null
@@ -0,0 +1,71 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DismissNotificationPage
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.web.Method.GET
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DismissNotificationPage].
+ */
+class DismissNotificationPageTest: WebPageTest() {
+
+       private val page = DismissNotificationPage(template, webInterface)
+       private val notification = mock<Notification>()
+
+       override fun getPage() = page
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("dismissNotification.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.DismissNotification.Title")).thenReturn("dismiss notification page")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("dismiss notification page"))
+       }
+
+       @Test
+       fun `get request with invalid notification ID redirects to return page`() {
+               request("", GET)
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html")
+       }
+
+       @Test
+       fun `get request with non-dismissible notification never dismisses the notification but redirects to return page`() {
+               request("", GET)
+               addNotification("notification-id", notification)
+               addHttpRequestParameter("notification", "notification-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(notification, never()).dismiss()
+               }
+       }
+
+       @Test
+       fun `get request with dismissible notification dismisses the notification and redirects to return page`() {
+               request("", GET)
+               whenever(notification.isDismissable).thenReturn(true)
+               addNotification("notification-id", notification)
+               addHttpRequestParameter("notification", "notification-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(notification).dismiss()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/DistrustPageTest.kt
new file mode 100644 (file)
index 0000000..45498c2
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.DistrustPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DistrustPage].
+ */
+class DistrustPageTest: WebPageTest() {
+
+       private val page = DistrustPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("distrust.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.Distrust.Title")).thenReturn("distrust page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("distrust page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid sone redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html")
+       }
+
+       @Test
+       fun `post request with valid sone distrusts sone and redirects to return page`() {
+               request("", POST)
+               val remoteSone = mock<Sone>()
+               addSone("remote-sone-id", remoteSone)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "remote-sone-id")
+               verifyRedirect("return.html") {
+                       verify(core).distrustSone(currentSone, remoteSone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditAlbumPageTest.kt
new file mode 100644 (file)
index 0000000..c9bd4fb
--- /dev/null
@@ -0,0 +1,131 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Album.Modifier
+import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.mockBuilder
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.EditAlbumPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditAlbumPage].
+ */
+class EditAlbumPageTest: WebPageTest() {
+
+       private val page = EditAlbumPage(template, webInterface)
+
+       private val album = mock<Album>()
+       private val parentAlbum = mock<Album>()
+       private val modifier = mockBuilder<Modifier>()
+       private val sone = mock<Sone>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setup() {
+               whenever(album.id).thenReturn("album-id")
+               whenever(album.sone).thenReturn(sone)
+               whenever(album.parent).thenReturn(parentAlbum)
+               whenever(album.modify()).thenReturn(modifier)
+               whenever(modifier.update()).thenReturn(album)
+               whenever(parentAlbum.id).thenReturn("parent-id")
+               whenever(sone.isLocal).thenReturn(true)
+               addHttpRequestHeader("Host", "www.te.st")
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("editAlbum.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.EditAlbum.Title")).thenReturn("edit album page")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("edit album page"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid album redirects to invalid page`() {
+               request("", POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with album of non-local sone redirects to no permissions page`() {
+               request("", POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with move left requested moves album to the left and redirects to album browser`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("moveLeft", "true")
+               verifyRedirect("imageBrowser.html?album=parent-id") {
+                       verify(parentAlbum).moveAlbumUp(album)
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with move right requested moves album to the left and redirects to album browser`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("moveRight", "true")
+               verifyRedirect("imageBrowser.html?album=parent-id") {
+                       verify(parentAlbum).moveAlbumDown(album)
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with empty album title redirects to empty album title page`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               whenever(modifier.setTitle("")).thenThrow(AlbumTitleMustNotBeEmpty())
+               verifyRedirect("emptyAlbumTitle.html")
+       }
+
+       @Test
+       fun `post request with non-empty album title and description redirects to album browser`() {
+               request("", POST)
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("title", "title")
+               addHttpRequestParameter("description", "description")
+               verifyRedirect("imageBrowser.html?album=album-id") {
+                       verify(modifier).setTitle("title")
+                       verify(modifier).setDescription("description")
+                       verify(modifier).update()
+                       verify(core).touchConfiguration()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditImagePageTest.kt
new file mode 100644 (file)
index 0000000..d36dec5
--- /dev/null
@@ -0,0 +1,135 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Image.Modifier
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.mockBuilder
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.EditImagePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditImagePage].
+ */
+class EditImagePageTest : WebPageTest() {
+
+       private val page = EditImagePage(template, webInterface)
+
+       private val image = mock<Image>()
+       private val modifier = mockBuilder<Modifier>()
+       private val sone = mock<Sone>()
+       private val album = mock<Album>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupImage() {
+               whenever(sone.isLocal).thenReturn(true)
+               whenever(album.id).thenReturn("album-id")
+               whenever(modifier.update()).thenReturn(image)
+               whenever(image.sone).thenReturn(sone)
+               whenever(image.album).thenReturn(album)
+               whenever(image.modify()).thenReturn(modifier)
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid image redirects to invalid page`() {
+               request("", POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with valid image from non-local sone redirects to no permission page`() {
+               request("", POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with valid image and move left requested moves image left and redirects to return page`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("moveLeft", "true")
+               verifyRedirect("return.html") {
+                       verify(album).moveImageUp(image)
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with valid image and move right requested moves image right and redirects to return page`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("moveRight", "true")
+               verifyRedirect("return.html") {
+                       verify(album).moveImageDown(image)
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with valid image but only whitespace in the title redirects to empty image title page`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("title", "   ")
+               verifyRedirect("emptyImageTitle.html") {
+                       verify(core, never()).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with valid image title and description modifies image and redirects to reutrn page`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("title", "Title")
+               addHttpRequestParameter("description", "Description")
+               verifyRedirect("return.html") {
+                       verify(modifier).setTitle("Title")
+                       verify(modifier).setDescription("Description")
+                       verify(modifier).update()
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with image title and description modifies image with filtered description and redirects to reutrn page`() {
+               request("", POST)
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("title", "Title")
+               addHttpRequestHeader("Host", "www.te.st")
+               addHttpRequestParameter("description", "Get http://www.te.st/KSK@GPL.txt")
+               verifyRedirect("return.html") {
+                       verify(modifier).setTitle("Title")
+                       verify(modifier).setDescription("Get KSK@GPL.txt")
+                       verify(modifier).update()
+                       verify(core).touchConfiguration()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPageTest.kt
new file mode 100644 (file)
index 0000000..cbe1289
--- /dev/null
@@ -0,0 +1,89 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.EditProfileFieldPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditProfileFieldPage].
+ */
+class EditProfileFieldPageTest : WebPageTest() {
+
+       private val page = EditProfileFieldPage(template, webInterface)
+
+       private val profile = Profile(currentSone)
+       private val field = profile.addField("Name")
+
+       override fun getPage() = page
+
+       @Before
+       fun setupProfile() {
+               whenever(currentSone.profile).thenReturn(profile)
+       }
+
+       @Test
+       fun `get request with invalid field redirects to invalid page`() {
+               request("", GET)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid field stores field in template context`() {
+               request("", GET)
+               addHttpRequestParameter("field", field.id)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["field"], equalTo<Any>(field))
+       }
+
+       @Test
+       fun `post request with cancel set redirects to profile edit page`() {
+               request("", POST)
+               addHttpRequestParameter("field", field.id)
+               addHttpRequestParameter("cancel", "true")
+               verifyRedirect("editProfile.html#profile-fields")
+       }
+
+       @Test
+       fun `post request with new name renames field and redirects to profile edit page`() {
+               request("", POST)
+               addHttpRequestParameter("field", field.id)
+               addHttpRequestParameter("name", "New Name")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(field.name, equalTo("New Name"))
+                       verify(currentSone).profile = profile
+               }
+       }
+
+       @Test
+       fun `post request with same name does not modify field and redirects to profile edit page`() {
+               request("", POST)
+               addHttpRequestParameter("field", field.id)
+               addHttpRequestParameter("name", "Name")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(field.name, equalTo("Name"))
+                       verify(currentSone, never()).profile = profile
+               }
+       }
+
+       @Test
+       fun `post request with same name as different field sets error condition in template`() {
+               request("", POST)
+               profile.addField("New Name")
+               addHttpRequestParameter("field", field.id)
+               addHttpRequestParameter("name", "New Name")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(field.name, equalTo("Name"))
+               verify(currentSone, never()).profile = profile
+               assertThat(templateContext["duplicateFieldName"], equalTo<Any>(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/EditProfilePageTest.kt
new file mode 100644 (file)
index 0000000..c79bbcf
--- /dev/null
@@ -0,0 +1,212 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.EditProfilePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.notNullValue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditProfilePage].
+ */
+class EditProfilePageTest : WebPageTest() {
+
+       private val page = EditProfilePage(template, webInterface)
+
+       private val profile = Profile(currentSone)
+       private val firstField = profile.addField("First Field")
+       private val secondField = profile.addField("Second Field")
+
+       override fun getPage() = page
+
+       @Before
+       fun setupProfile() {
+               val avatar = mock<Image>()
+               whenever(avatar.id).thenReturn("image-id")
+               whenever(avatar.sone).thenReturn(currentSone)
+               profile.firstName = "First"
+               profile.middleName = "Middle"
+               profile.lastName = "Last"
+               profile.birthDay = 31
+               profile.birthMonth = 12
+               profile.birthYear = 1999
+               profile.setAvatar(avatar)
+               whenever(currentSone.profile).thenReturn(profile)
+       }
+
+       @Test
+       fun `get request stores fields of current sone’s profile in template context`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["firstName"], equalTo<Any>("First"))
+               assertThat(templateContext["middleName"], equalTo<Any>("Middle"))
+               assertThat(templateContext["lastName"], equalTo<Any>("Last"))
+               assertThat(templateContext["birthDay"], equalTo<Any>(31))
+               assertThat(templateContext["birthMonth"], equalTo<Any>(12))
+               assertThat(templateContext["birthYear"], equalTo<Any>(1999))
+               assertThat(templateContext["avatarId"], equalTo<Any>("image-id"))
+               assertThat(templateContext["fields"], equalTo<Any>(listOf(firstField, secondField)))
+       }
+
+       @Test
+       fun `post request without any command stores fields of current sone’s profile in template context`() {
+               request("", POST)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["firstName"], equalTo<Any>("First"))
+               assertThat(templateContext["middleName"], equalTo<Any>("Middle"))
+               assertThat(templateContext["lastName"], equalTo<Any>("Last"))
+               assertThat(templateContext["birthDay"], equalTo<Any>(31))
+               assertThat(templateContext["birthMonth"], equalTo<Any>(12))
+               assertThat(templateContext["birthYear"], equalTo<Any>(1999))
+               assertThat(templateContext["avatarId"], equalTo<Any>("image-id"))
+               assertThat(templateContext["fields"], equalTo<Any>(listOf(firstField, secondField)))
+       }
+
+       private fun <T> verifySingleFieldCanBeChanged(fieldName: String, newValue: T, expectedValue: T = newValue, fieldAccessor: () -> T) {
+               request("", POST)
+               addHttpRequestParameter("save-profile", "true")
+               addHttpRequestParameter(fieldName, newValue.toString())
+               verifyRedirect("editProfile.html") {
+                       verify(core).touchConfiguration()
+                       assertThat(fieldAccessor(), equalTo(expectedValue))
+               }
+       }
+
+       @Test
+       fun `post request with new first name and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("first-name", "New First") { profile.firstName }
+       }
+
+       @Test
+       fun `post request with new middle name and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("middle-name", "New Middle") { profile.middleName }
+       }
+
+       @Test
+       fun `post request with new last name and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("last-name", "New Last") { profile.lastName }
+       }
+
+       @Test
+       fun `post request with new birth day and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("birth-day", 1) { profile.birthDay }
+       }
+
+       @Test
+       fun `post request with new birth month and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("birth-month", 1) { profile.birthMonth }
+       }
+
+       @Test
+       fun `post request with new birth year and save profile saves the profile and redirects back to profile edit page`() {
+               verifySingleFieldCanBeChanged("birth-year", 1) { profile.birthYear }
+       }
+
+       @Test
+       fun `post request with new avatar ID and save profile saves the profile and redirects back to profile edit page`() {
+               val newAvatar = mock<Image>()
+               whenever(newAvatar.sone).thenReturn(currentSone)
+               whenever(newAvatar.id).thenReturn("avatar-id")
+               addImage("avatar-id", newAvatar)
+               verifySingleFieldCanBeChanged("avatarId", "avatar-id") { profile.avatar }
+       }
+
+       @Test
+       fun `post request with field value saves profile and redirects back to profile edit page`() {
+               val field = profile.addField("name")
+               field.value = "old"
+               verifySingleFieldCanBeChanged("field-${field.id}", "new") { profile.getFieldByName("name")!!.value }
+       }
+
+       @Test
+       fun `post request with field value saves filtered value to profile and redirects back to profile edit page`() {
+               val field = profile.addField("name")
+               field.value = "old"
+               addHttpRequestHeader("Host", "www.te.st")
+               verifySingleFieldCanBeChanged("field-${field.id}", "http://www.te.st/KSK@GPL.txt", "KSK@GPL.txt") { profile.getFieldByName("name")!!.value }
+       }
+
+       @Test
+       fun `adding a field with a duplicate name sets error in template context`() {
+               request("", POST)
+               profile.addField("new-field")
+               addHttpRequestParameter("add-field", "true")
+               addHttpRequestParameter("field-name", "new-field")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["fieldName"], equalTo<Any>("new-field"))
+               assertThat(templateContext["duplicateFieldName"], equalTo<Any>(true))
+               verify(core, never()).touchConfiguration()
+       }
+
+       @Test
+       fun `adding a field with a new name sets adds field to profile and redirects to profile edit page`() {
+               request("", POST)
+               addHttpRequestParameter("add-field", "true")
+               addHttpRequestParameter("field-name", "new-field")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(profile.getFieldByName("new-field"), notNullValue())
+                       verify(currentSone).profile = profile
+                       verify(core).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `deleting a field redirects to delete field page`() {
+               request("", POST)
+               addHttpRequestParameter("delete-field-${firstField.id}", "true")
+               verifyRedirect("deleteProfileField.html?field=${firstField.id}")
+       }
+
+       @Test
+       fun `moving a field up moves the field up and redirects to the edit profile page`() {
+               request("", POST)
+               addHttpRequestParameter("move-up-field-${secondField.id}", "true")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(profile.fields, contains(secondField, firstField))
+                       verify(currentSone).profile = profile
+               }
+       }
+
+       @Test
+       fun `moving an invalid field up redirects to the invalid page`() {
+               request("", POST)
+               addHttpRequestParameter("move-up-field-foo", "true")
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `moving a field down moves the field down and redirects to the edit profile page`() {
+               request("", POST)
+               addHttpRequestParameter("move-down-field-${firstField.id}", "true")
+               verifyRedirect("editProfile.html#profile-fields") {
+                       assertThat(profile.fields, contains(secondField, firstField))
+                       verify(currentSone).profile = profile
+               }
+       }
+
+       @Test
+       fun `moving an invalid field down redirects to the invalid page`() {
+               request("", POST)
+               addHttpRequestParameter("move-down-field-foo", "true")
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `editing a field redirects to the edit profile page`() {
+               request("", POST)
+               addHttpRequestParameter("edit-field-${firstField.id}", "true")
+               verifyRedirect("editProfileField.html?field=${firstField.id}")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/FollowSonePageTest.kt
new file mode 100644 (file)
index 0000000..f595b4a
--- /dev/null
@@ -0,0 +1,72 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.FollowSonePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [FollowSonePage].
+ */
+class FollowSonePageTest : WebPageTest() {
+
+       private val page = FollowSonePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `a single sone can be followed`() {
+               request("", POST)
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               addHttpRequestParameter("sone", "sone-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core).followSone(currentSone, "sone-id")
+                       verify(core).markSoneKnown(sone)
+               }
+       }
+
+       @Test
+       fun `multiple sones can be followed`() {
+               request("", POST)
+               val firstSone = mock<Sone>()
+               addSone("sone-id1", firstSone)
+               val secondSone = mock<Sone>()
+               addSone("sone-id2", secondSone)
+               addHttpRequestParameter("sone", "sone-id1,sone-id2")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core).followSone(currentSone, "sone-id1")
+                       verify(core).followSone(currentSone, "sone-id2")
+                       verify(core).markSoneKnown(firstSone)
+                       verify(core).markSoneKnown(secondSone)
+               }
+       }
+
+       @Test
+       fun `a non-existing sone is not followed`() {
+               request("", POST)
+               addHttpRequestParameter("sone", "sone-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).followSone(ArgumentMatchers.eq(currentSone), anyString())
+                       verify(core, never()).markSoneKnown(any<Sone>())
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/GetImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/GetImagePageTest.kt
new file mode 100644 (file)
index 0000000..fb96a4c
--- /dev/null
@@ -0,0 +1,53 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.TemporaryImage
+import net.pterodactylus.sone.web.pages.GetImagePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetImagePage].
+ */
+class GetImagePageTest : WebPageTest() {
+
+       private val page = GetImagePage(webInterface)
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("getImage.html"))
+       }
+
+       @Test
+       fun `page is not a prefix page`() {
+               assertThat(page.isPrefixPage, equalTo(false))
+       }
+
+       @Test
+       fun `page is not link-excepted`() {
+               assertThat(page.isLinkExcepted(null), equalTo(false))
+       }
+
+       @Test
+       fun `invalid image returns 404 response`() {
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(404))
+               assertThat(responseBytes, equalTo(ByteArray(0)))
+       }
+
+       @Test
+       fun `valid image returns response with correct data`() {
+               val image = TemporaryImage("temp-id").apply {
+                       mimeType = "image/test"
+                       imageData = ByteArray(5, Int::toByte)
+               }
+               addHttpRequestParameter("image", "temp-id")
+               addTemporaryImage("temp-id", image)
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.contentType, equalTo("image/test"))
+               assertThat(responseBytes, equalTo(ByteArray(5, Int::toByte)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPageTest.kt
new file mode 100644 (file)
index 0000000..7a12242
--- /dev/null
@@ -0,0 +1,106 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.ImageBrowserPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [ImageBrowserPage].
+ */
+class ImageBrowserPageTest : WebPageTest() {
+
+       private val page = ImageBrowserPage(template, webInterface)
+
+       @Test
+       fun `get request with album sets album and page in template context`() {
+               request("", GET)
+               val album = mock<Album>()
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("page", "5")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["albumRequested"], equalTo<Any>(true))
+               assertThat(templateContext["album"], equalTo<Any>(album))
+               assertThat(templateContext["page"], equalTo<Any>("5"))
+       }
+
+       @Test
+       fun `get request with image sets image in template context`() {
+               request("", GET)
+               val image = mock<Image>()
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["imageRequested"], equalTo<Any>(true))
+               assertThat(templateContext["image"], equalTo<Any>(image))
+       }
+
+       @Test
+       fun `get request with sone sets sone in template context`() {
+               request("", GET)
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               addHttpRequestParameter("sone", "sone-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["soneRequested"], equalTo<Any>(true))
+               assertThat(templateContext["sone"], equalTo<Any>(sone))
+       }
+
+       @Test
+       fun `get request with mode of gallery sets albums and page in template context`() {
+               request("", GET)
+               val firstSone = createSone("first album", "second album")
+               addSone("sone1", firstSone)
+               val secondSone = createSone("third album", "fourth album")
+               addSone("sone2", secondSone)
+               addHttpRequestParameter("mode", "gallery")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["galleryRequested"], equalTo<Any>(true))
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["albums"] as Iterable<Album>, contains(
+                               firstSone.rootAlbum.albums[0],
+                               secondSone.rootAlbum.albums[1],
+                               firstSone.rootAlbum.albums[1],
+                               secondSone.rootAlbum.albums[0]
+               ))
+       }
+
+       private fun createSone(firstAlbumTitle: String, secondAlbumTitle: String): Sone {
+               return mock<Sone>().apply {
+                       val rootAlbum = mock<Album>()
+                       val firstAlbum = mock<Album>()
+                       val firstImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
+                       whenever(firstAlbum.images).thenReturn(listOf(firstImage))
+                       val secondAlbum = mock<Album>()
+                       val secondImage = mock<Image>().run { whenever(isInserted).thenReturn(true); this }
+                       whenever(secondAlbum.images).thenReturn(listOf(secondImage))
+                       whenever(firstAlbum.title).thenReturn(firstAlbumTitle)
+                       whenever(secondAlbum.title).thenReturn(secondAlbumTitle)
+                       whenever(rootAlbum.albums).thenReturn(listOf(firstAlbum, secondAlbum))
+                       whenever(this.rootAlbum).thenReturn(rootAlbum)
+               }
+       }
+
+       @Test
+       fun `requesting nothing will show the albums of the current sone`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["soneRequested"], equalTo<Any>(true))
+               assertThat(templateContext["sone"], equalTo<Any>(currentSone))
+       }
+
+       @Test
+       fun `page is link-excepted`() {
+           assertThat(page.isLinkExcepted(null), equalTo(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/IndexPageTest.kt
new file mode 100644 (file)
index 0000000..04c83d6
--- /dev/null
@@ -0,0 +1,82 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Optional.fromNullable
+import com.google.common.base.Predicate
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.notify.PostVisibilityFilter
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.IndexPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+
+/**
+ * Unit test for [IndexPage].
+ */
+class IndexPageTest : WebPageTest() {
+
+       private val postVisibilityFilter = mock<PostVisibilityFilter>()
+       private val page = IndexPage(template, webInterface, postVisibilityFilter)
+
+       @Before
+       fun setupPostVisibilityFilter() {
+               whenever(postVisibilityFilter.isVisible(ArgumentMatchers.eq(currentSone))).thenReturn(object : Predicate<Post> {
+                       override fun apply(input: Post?) = true
+               })
+       }
+
+       private fun createPost(time: Long, directed: Boolean = false) = mock<Post>().apply {
+               whenever(this.time).thenReturn(time)
+               whenever(recipient).thenReturn(fromNullable(if (directed) currentSone else null))
+       }
+
+       @Test
+       fun `index page shows all posts of current sone`() {
+               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
+               whenever(currentSone.posts).thenReturn(posts)
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(*posts.toTypedArray()))
+       }
+
+       @Test
+       fun `index page shows posts directed at current sone from non-followed sones`() {
+               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
+               whenever(currentSone.posts).thenReturn(posts)
+               val notFollowedSone = mock<Sone>()
+               val notFollowedPosts = listOf(createPost(2500, true), createPost(1500))
+               whenever(notFollowedSone.posts).thenReturn(notFollowedPosts)
+               addSone("notfollowed1", notFollowedSone)
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(
+                               posts[0], notFollowedPosts[0], posts[1], posts[2]
+               ))
+       }
+
+       @Test
+       fun `index page does not show duplicate posts`() {
+               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
+               whenever(currentSone.posts).thenReturn(posts)
+               val followedSone = mock<Sone>()
+               val followedPosts = listOf(createPost(2500, true), createPost(1500))
+               whenever(followedSone.posts).thenReturn(followedPosts)
+               whenever(currentSone.friends).thenReturn(listOf("followed1", "followed2"))
+               addSone("followed1", followedSone)
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(
+                               posts[0], followedPosts[0], posts[1], followedPosts[1], posts[2]
+               ))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPageTest.kt
new file mode 100644 (file)
index 0000000..0476c50
--- /dev/null
@@ -0,0 +1,184 @@
+package net.pterodactylus.sone.web.pages
+
+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.Sone
+import net.pterodactylus.sone.freenet.wot.Identity
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.KnownSonesPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [KnownSonesPage].
+ */
+class KnownSonesPageTest : WebPageTest() {
+
+       private val page = KnownSonesPage(template, webInterface)
+
+       private val sones = listOf(
+                       createSone(1000, 4, 7, 2, "sone2", true, true),
+                       createSone(2000, 3, 2, 3, "Sone1", false, true),
+                       createSone(3000, 3, 8, 1, "Sone3", true, false),
+                       createSone(4000, 1, 6, 0, "sone0", false, false)
+       )
+
+       @Before
+       fun setupSones() {
+               addSone("sone1", sones[0])
+               addSone("sone2", sones[1])
+               addSone("sone3", sones[2])
+               addSone("sone4", sones[3])
+       }
+
+       private fun createSone(time: Long, posts: Int, replies: Int, images: Int, name: String, local: Boolean, new: Boolean) = mock<Sone>().apply {
+               whenever(identity).thenReturn(if (local) mock<OwnIdentity>() else mock<Identity>())
+               whenever(isKnown).thenReturn(!new)
+               whenever(this.time).thenReturn(time)
+               whenever(this.posts).thenReturn((0..(posts - 1)).map { mock<Post>() })
+               whenever(this.replies).thenReturn((0..(replies - 1)).map { mock<PostReply>() }.toSet())
+               val album = mock<Album>()
+               whenever(album.images).thenReturn(((0..(images - 1)).map { mock<Image>() }))
+               val rootAlbum = mock<Album>().apply {
+                       whenever(albums).thenReturn(listOf(album))
+               }
+               whenever(this.rootAlbum).thenReturn(rootAlbum)
+               whenever(this.profile).thenReturn(mock<Profile>())
+               whenever(id).thenReturn(name.toLowerCase())
+               whenever(this.name).thenReturn(name)
+       }
+
+       private fun verifySonesAreInOrder(vararg indices: Int) {
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["knownSones"] as Iterable<Sone>, contains(
+                               *indices.map { sones[it] }.toTypedArray()
+               ))
+       }
+
+       @Test
+       fun `default known sones are sorted newest first`() {
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 2, 1, 0)
+       }
+
+       @Test
+       fun `known sones can be sorted by oldest first`() {
+               addHttpRequestParameter("order", "asc")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(0, 1, 2, 3)
+       }
+
+       @Test
+       fun `known sones can be sorted by posts, most posts first`() {
+               addHttpRequestParameter("sort", "posts")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(0, 2, 1, 3)
+       }
+
+       @Test
+       fun `known sones can be sorted by posts, least posts first`() {
+               addHttpRequestParameter("sort", "posts")
+               addHttpRequestParameter("order", "asc")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 1, 2, 0)
+       }
+
+       @Test
+       fun `known sones can be sorted by images, most images first`() {
+               addHttpRequestParameter("sort", "images")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(1, 0, 2, 3)
+       }
+
+       @Test
+       fun `known sones can be sorted by images, least images first`() {
+               addHttpRequestParameter("sort", "images")
+               addHttpRequestParameter("order", "asc")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 2, 0, 1)
+       }
+
+       @Test
+       fun `known sones can be sorted by nice name, ascending`() {
+               addHttpRequestParameter("sort", "name")
+               addHttpRequestParameter("order", "asc")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 1, 0, 2)
+       }
+
+       @Test
+       fun `known sones can be sorted by nice name, descending`() {
+               addHttpRequestParameter("sort", "name")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(2, 0, 1, 3)
+       }
+
+       @Test
+       fun `known sones can be filtered by local sones`() {
+               addHttpRequestParameter("filter", "own")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(2, 0)
+       }
+
+       @Test
+       fun `known sones can be filtered by non-local sones`() {
+               addHttpRequestParameter("filter", "not-own")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 1)
+       }
+
+       @Test
+       fun `known sones can be filtered by new sones`() {
+               addHttpRequestParameter("filter", "new")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(1, 0)
+       }
+
+       @Test
+       fun `known sones can be filtered by known sones`() {
+               addHttpRequestParameter("filter", "not-new")
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 2)
+       }
+
+       @Test
+       fun `known sones can be filtered by followed sones`() {
+               addHttpRequestParameter("filter", "followed")
+               listOf("sone1", "sone3").forEach { whenever(currentSone.hasFriend(it)).thenReturn(true) }
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(2, 1)
+       }
+
+       @Test
+       fun `known sones can be filtered by not-followed sones`() {
+               addHttpRequestParameter("filter", "not-followed")
+               listOf("sone1", "sone3").forEach { whenever(currentSone.hasFriend(it)).thenReturn(true) }
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 0)
+       }
+
+       @Test
+       fun `known sones can not be filtered by followed sones if there is no current sone`() {
+               addHttpRequestParameter("filter", "followed")
+               unsetCurrentSone()
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 2, 1, 0)
+       }
+
+       @Test
+       fun `known sones can not be filtered by not-followed sones if there is no current sone`() {
+               addHttpRequestParameter("filter", "not-followed")
+               unsetCurrentSone()
+               page.handleRequest(freenetRequest, templateContext)
+               verifySonesAreInOrder(3, 2, 1, 0)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/LikePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/LikePageTest.kt
new file mode 100644 (file)
index 0000000..10baf5e
--- /dev/null
@@ -0,0 +1,58 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.pages.LikePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+/**
+ * Unit test for [LikePage].
+ */
+class LikePageTest : WebPageTest() {
+
+       private val page = LikePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with post id likes post and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("type", "post")
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(currentSone).addLikedPostId("post-id")
+               }
+       }
+
+       @Test
+       fun `post request with reply id likes post and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("type", "reply")
+               addHttpRequestParameter("reply", "reply-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(currentSone).addLikedReplyId("reply-id")
+               }
+       }
+
+       @Test
+       fun `post request with invalid likes redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("type", "foo")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verifyNoMoreInteractions(currentSone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/LockSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/LockSonePageTest.kt
new file mode 100644 (file)
index 0000000..9a6876c
--- /dev/null
@@ -0,0 +1,40 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.LockSonePage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [LockSonePage].
+ */
+class LockSonePageTest : WebPageTest() {
+
+       private val page = LockSonePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `locking an invalid local sone redirects to return page`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).lockSone(any<Sone>())
+               }
+       }
+
+       @Test
+       fun `locking an valid local sone locks the sone and redirects to return page`() {
+               addHttpRequestParameter("sone", "sone-id")
+               val sone = mock<Sone>()
+               addLocalSone("sone-id", sone)
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core).lockSone(sone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/LoginPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/LoginPageTest.kt
new file mode 100644 (file)
index 0000000..0da820c
--- /dev/null
@@ -0,0 +1,150 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.freenet.wot.Identity
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.thenReturnMock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.WebPageTest
+import net.pterodactylus.sone.web.pages.LoginPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [LoginPage].
+ */
+class LoginPageTest : WebPageTest() {
+
+       private val page = LoginPage(template, webInterface)
+
+       private val sones = listOf(createSone("Sone", "Test"), createSone("Test"), createSone("Sone"))
+
+       override fun getPage() = page
+
+       private fun createSone(vararg contexts: String) = mock<Sone>().apply {
+               whenever(id).thenReturn(hashCode().toString())
+               val identity = mock<OwnIdentity>().apply {
+                       whenever(this.contexts).thenReturn(contexts.toSet())
+                       contexts.forEach { whenever(hasContext(it)).thenReturn(true) }
+               }
+               whenever(this.identity).thenReturn(identity)
+               whenever(profile).thenReturnMock()
+       }
+
+       @Before
+       fun setupSones() {
+               addLocalSone("sone1", sones[0])
+               addLocalSone("sone2", sones[1])
+               addLocalSone("sone3", sones[2])
+               addOwnIdentity(sones[0].identity as OwnIdentity)
+               addOwnIdentity(sones[1].identity as OwnIdentity)
+               addOwnIdentity(sones[2].identity as OwnIdentity)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("login.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+           assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `get request stores sones in template context`() {
+               request("", GET)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["sones"] as Iterable<Sone>, containsInAnyOrder(sones[0], sones[1], sones[2]))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `get request stores identities without sones in template context`() {
+               request("", GET)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["identitiesWithoutSone"] as Iterable<Identity>, contains(sones[1].identity))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `post request with invalid sone sets sones and identities without sone in template context`() {
+               request("", POST)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["sones"] as Iterable<Sone>, containsInAnyOrder(sones[0], sones[1], sones[2]))
+               assertThat(templateContext["identitiesWithoutSone"] as Iterable<Identity>, contains(sones[1].identity))
+       }
+
+       @Test
+       fun `post request with valid sone logs in the sone and redirects to index page`() {
+               request("", POST)
+               addHttpRequestParameter("sone-id", "sone2")
+               verifyRedirect("index.html") {
+                       verify(webInterface).setCurrentSone(toadletContext, sones[1])
+               }
+       }
+
+       @Test
+       fun `post request with valid sone and target redirects to target page`() {
+               request("", POST)
+               addHttpRequestParameter("sone-id", "sone2")
+               addHttpRequestParameter("target", "foo.html")
+               verifyRedirect("foo.html") {
+                       verify(webInterface).setCurrentSone(toadletContext, sones[1])
+               }
+       }
+
+       @Test
+       fun `redirect to index html if a sone is logged in`() {
+               assertThat(page.getRedirectTarget(freenetRequest), equalTo("index.html"))
+       }
+
+       @Test
+       fun `do not redirect if no sone is logged in`() {
+               unsetCurrentSone()
+               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
+       }
+
+       @Test
+       fun `page is not enabled if full access required and request is not full access`() {
+               core.preferences.isRequireFullAccess = true
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is enabled if no full access is required and there is no current sone`() {
+               unsetCurrentSone()
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `page is not enabled if no full access is required but there is a current sone`() {
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is enabled if full access required and request is full access and there is no current sone`() {
+               core.preferences.isRequireFullAccess = true
+               unsetCurrentSone()
+               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `page is not enabled if full access required and request is full access but there is a current sone`() {
+               core.preferences.isRequireFullAccess = true
+               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/LogoutPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/LogoutPageTest.kt
new file mode 100644 (file)
index 0000000..14ba030
--- /dev/null
@@ -0,0 +1,59 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.LogoutPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [LogoutPage].
+ */
+class LogoutPageTest : WebPageTest() {
+
+       private val page = LogoutPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `page unsets current sone and redirects to index`() {
+               verifyRedirect("index.html") {
+                       verify(webInterface).setCurrentSone(toadletContext, null)
+               }
+       }
+
+       @Test
+       fun `page is not enabled if sone requires full access and request does not have full access`() {
+               core.preferences.isRequireFullAccess = true
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is disabled if no sone is logged in`() {
+               unsetCurrentSone()
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is disabled if sone is logged in but there is only one sone`() {
+               whenever(core.localSones).thenReturn(listOf(currentSone))
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is enabled if sone is logged in and there is more than one sone`() {
+               whenever(core.localSones).thenReturn(listOf(currentSone, currentSone))
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `page is enabled if full access is required and present and sone is logged in and there is more than one sone`() {
+               core.preferences.isRequireFullAccess = true
+               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
+               whenever(core.localSones).thenReturn(listOf(currentSone, currentSone))
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPageTest.kt
new file mode 100644 (file)
index 0000000..cc19b60
--- /dev/null
@@ -0,0 +1,69 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.MarkAsKnownPage
+import net.pterodactylus.sone.web.pages.WebPageTest
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [MarkAsKnownPage].
+ */
+class MarkAsKnownPageTest : WebPageTest() {
+
+       private val page = MarkAsKnownPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `posts can be marked as known`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("type", "post")
+               addHttpRequestParameter("id", "post1 post2 post3")
+               val posts = listOf(mock<Post>(), mock<Post>())
+               addPost("post1", posts[0])
+               addPost("post3", posts[1])
+               verifyRedirect("return.html") {
+                       verify(core).markPostKnown(posts[0])
+                       verify(core).markPostKnown(posts[1])
+               }
+       }
+
+       @Test
+       fun `replies can be marked as known`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("type", "reply")
+               addHttpRequestParameter("id", "reply1 reply2 reply3")
+               val replies = listOf(mock<PostReply>(), mock<PostReply>())
+               addPostReply("reply1", replies[0])
+               addPostReply("reply3", replies[1])
+               verifyRedirect("return.html") {
+                       verify(core).markReplyKnown(replies[0])
+                       verify(core).markReplyKnown(replies[1])
+               }
+       }
+
+       @Test
+       fun `sones can be marked as known`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("type", "sone")
+               addHttpRequestParameter("id", "sone1 sone2 sone3")
+               val sones = listOf(mock<Sone>(), mock<Sone>())
+               addSone("sone1", sones[0])
+               addSone("sone3", sones[1])
+               verifyRedirect("return.html") {
+                       verify(core).markSoneKnown(sones[0])
+                       verify(core).markSoneKnown(sones[1])
+               }
+       }
+
+       @Test
+       fun `different type redirects to invalid page`() {
+               addHttpRequestParameter("type", "foo")
+               verifyRedirect("invalid.html")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/OptionsPageTest.kt
new file mode 100644 (file)
index 0000000..950a3fe
--- /dev/null
@@ -0,0 +1,343 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.FOLLOWED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.OptionsPage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [OptionsPage].
+ */
+class OptionsPageTest : WebPageTest() {
+
+       private val page = OptionsPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Before
+       fun setupPreferences() {
+               core.preferences.insertionDelay = 1
+               core.preferences.charactersPerPost = 50
+               core.preferences.fcpFullAccessRequired = WRITING
+               core.preferences.imagesPerPage = 4
+               core.preferences.isFcpInterfaceActive = true
+               core.preferences.isRequireFullAccess = true
+               core.preferences.negativeTrust = 7
+               core.preferences.positiveTrust = 8
+               core.preferences.postCutOffLength = 51
+               core.preferences.postsPerPage = 10
+               core.preferences.trustComment = "11"
+       }
+
+       @Before
+       fun setupSoneOptions() {
+               whenever(currentSone.options).thenReturn(DefaultSoneOptions().apply {
+                       isAutoFollow = true
+                       isShowNewPostNotifications = true
+                       isShowNewReplyNotifications = true
+                       isShowNewSoneNotifications = true
+                       isSoneInsertNotificationEnabled = true
+                       loadLinkedImages = FOLLOWED
+                       showCustomAvatars = FOLLOWED
+               })
+       }
+
+       @Test
+       fun `get request stores all preferences in the template context`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["auto-follow"], equalTo<Any>(true))
+               assertThat(templateContext["show-notification-new-sones"], equalTo<Any>(true))
+               assertThat(templateContext["show-notification-new-posts"], equalTo<Any>(true))
+               assertThat(templateContext["show-notification-new-replies"], equalTo<Any>(true))
+               assertThat(templateContext["enable-sone-insert-notifications"], equalTo<Any>(true))
+               assertThat(templateContext["load-linked-images"], equalTo<Any>("FOLLOWED"))
+               assertThat(templateContext["show-custom-avatars"], equalTo<Any>("FOLLOWED"))
+               assertThat(templateContext["insertion-delay"], equalTo<Any>(1))
+               assertThat(templateContext["characters-per-post"], equalTo<Any>(50))
+               assertThat(templateContext["fcp-full-access-required"], equalTo<Any>(1))
+               assertThat(templateContext["images-per-page"], equalTo<Any>(4))
+               assertThat(templateContext["fcp-interface-active"], equalTo<Any>(true))
+               assertThat(templateContext["require-full-access"], equalTo<Any>(true))
+               assertThat(templateContext["negative-trust"], equalTo<Any>(7))
+               assertThat(templateContext["positive-trust"], equalTo<Any>(8))
+               assertThat(templateContext["post-cut-off-length"], equalTo<Any>(51))
+               assertThat(templateContext["posts-per-page"], equalTo<Any>(10))
+               assertThat(templateContext["trust-comment"], equalTo<Any>("11"))
+       }
+
+       @Test
+       fun `get request without sone does not store sone-specific preferences in the template context`() {
+               request("", GET)
+               unsetCurrentSone()
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["auto-follow"], nullValue())
+               assertThat(templateContext["show-notification-new-sones"], nullValue())
+               assertThat(templateContext["show-notification-new-posts"], nullValue())
+               assertThat(templateContext["show-notification-new-replies"], nullValue())
+               assertThat(templateContext["enable-sone-insert-notifications"], nullValue())
+               assertThat(templateContext["load-linked-images"], nullValue())
+               assertThat(templateContext["show-custom-avatars"], nullValue())
+       }
+
+       private fun <T> verifyThatOptionCanBeSet(option: String, setValue: Any?, expectedValue: T, getter: () -> T) {
+               request("", POST)
+               addHttpRequestParameter(option, setValue.toString())
+               addHttpRequestParameter("show-custom-avatars", "ALWAYS")
+               addHttpRequestParameter("load-linked-images", "ALWAYS")
+               verifyRedirect("options.html") {
+                       assertThat(getter(), equalTo(expectedValue))
+               }
+       }
+
+       @Test
+       fun `auto-follow option can be set`() {
+               verifyThatOptionCanBeSet("auto-follow", "checked", true) { currentSone.options.isAutoFollow }
+       }
+
+       @Test
+       fun `show new sone notification option can be set`() {
+               verifyThatOptionCanBeSet("show-notification-new-sones", "checked", true) { currentSone.options.isShowNewSoneNotifications }
+       }
+
+       @Test
+       fun `show new post notification option can be set`() {
+               verifyThatOptionCanBeSet("show-notification-new-posts", "checked", true) { currentSone.options.isShowNewPostNotifications }
+       }
+
+       @Test
+       fun `show new reply notification option can be set`() {
+               verifyThatOptionCanBeSet("show-notification-new-replies", "checked", true) { currentSone.options.isShowNewReplyNotifications }
+       }
+
+       @Test
+       fun `enable sone insert notifications option can be set`() {
+               verifyThatOptionCanBeSet("enable-sone-insert-notifications", "checked", true) { currentSone.options.isSoneInsertNotificationEnabled }
+       }
+
+       @Test
+       fun `load linked images option can be set`() {
+               verifyThatOptionCanBeSet("load-linked-images", "TRUSTED", TRUSTED) { currentSone.options.loadLinkedImages }
+       }
+
+       @Test
+       fun `show custom avatar option can be set`() {
+               verifyThatOptionCanBeSet("show-custom-avatars", "TRUSTED", TRUSTED) { currentSone.options.showCustomAvatars }
+       }
+
+       private fun verifyThatWrongValueForPreferenceIsDetected(name: String, value: String) {
+               unsetCurrentSone()
+               request("", POST)
+               addHttpRequestParameter(name, value)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["fieldErrors"] as Iterable<*>, hasItem(name))
+       }
+
+       private fun <T> verifyThatPreferencesCanBeSet(name: String, setValue: String?, expectedValue: T, getter: () -> T) {
+               unsetCurrentSone()
+               request("", POST)
+               addHttpRequestParameter(name, setValue)
+               verifyRedirect("options.html") {
+                       assertThat(getter(), equalTo(expectedValue))
+               }
+       }
+
+       @Test
+       fun `insertion delay can not be set to less than 0 seconds`() {
+               verifyThatWrongValueForPreferenceIsDetected("insertion-delay", "-1")
+       }
+
+       @Test
+       fun `insertion delay can be set to 0 seconds`() {
+               verifyThatPreferencesCanBeSet("insertion-delay", "0", 0) { core.preferences.insertionDelay }
+       }
+
+       @Test
+       fun `setting insertion to an invalid value will reset it`() {
+               verifyThatPreferencesCanBeSet("insertion-delay", "foo", 60) { core.preferences.insertionDelay }
+       }
+
+       @Test
+       fun `characters per post can not be set to less than -1`() {
+               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "-2")
+       }
+
+       @Test
+       fun `characters per post can be set to -1`() {
+               verifyThatPreferencesCanBeSet("characters-per-post", "-1", -1) { core.preferences.charactersPerPost }
+       }
+
+       @Test
+       fun `characters per post can not be set to 0`() {
+               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "0")
+       }
+
+       @Test
+       fun `characters per post can not be set to 49`() {
+               verifyThatWrongValueForPreferenceIsDetected("characters-per-post", "49")
+       }
+
+       @Test
+       fun `characters per post can be set to 50`() {
+               verifyThatPreferencesCanBeSet("characters-per-post", "50", 50) { core.preferences.charactersPerPost }
+       }
+
+       @Test
+       fun `fcp full acess required option can be set to always`() {
+               verifyThatPreferencesCanBeSet("fcp-full-access-required", "2", ALWAYS) { core.preferences.fcpFullAccessRequired }
+       }
+
+       @Test
+       fun `fcp full acess required option can be set to writing`() {
+               verifyThatPreferencesCanBeSet("fcp-full-access-required", "1", WRITING) { core.preferences.fcpFullAccessRequired }
+       }
+
+       @Test
+       fun `fcp full acess required option can be set to no`() {
+               verifyThatPreferencesCanBeSet("fcp-full-access-required", "0", NO) { core.preferences.fcpFullAccessRequired }
+       }
+
+       @Test
+       fun `fcp full acess required option is not changed if invalid value is set`() {
+               verifyThatPreferencesCanBeSet("fcp-full-access-required", "foo", WRITING) { core.preferences.fcpFullAccessRequired }
+       }
+
+       @Test
+       fun `images per page can not be set to 0`() {
+               verifyThatWrongValueForPreferenceIsDetected("images-per-page", "0")
+       }
+
+       @Test
+       fun `images per page can be set to 1`() {
+               verifyThatPreferencesCanBeSet("images-per-page", "1", 1) { core.preferences.imagesPerPage }
+       }
+
+       @Test
+       fun `images per page is set to 9 if invalid value is requested`() {
+               verifyThatPreferencesCanBeSet("images-per-page", "foo", 9) { core.preferences.imagesPerPage }
+       }
+
+       @Test
+       fun `fcp interface can be set to true`() {
+               verifyThatPreferencesCanBeSet("fcp-interface-active", "checked", true) { core.preferences.isFcpInterfaceActive }
+       }
+
+       @Test
+       fun `fcp interface can be set to false`() {
+               verifyThatPreferencesCanBeSet("fcp-interface-active", null, false) { core.preferences.isFcpInterfaceActive }
+       }
+
+       @Test
+       fun `require full access can be set to true`() {
+               verifyThatPreferencesCanBeSet("require-full-access", "checked", true) { core.preferences.isRequireFullAccess }
+       }
+
+       @Test
+       fun `require full access can be set to false`() {
+               verifyThatPreferencesCanBeSet("require-full-access", null, false) { core.preferences.isRequireFullAccess }
+       }
+
+       @Test
+       fun `negative trust can not be set to -101`() {
+               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "-101")
+       }
+
+       @Test
+       fun `negative trust can be set to -100`() {
+               verifyThatPreferencesCanBeSet("negative-trust", "-100", -100) { core.preferences.negativeTrust }
+       }
+
+       @Test
+       fun `negative trust can be set to 100`() {
+               verifyThatPreferencesCanBeSet("negative-trust", "100", 100) { core.preferences.negativeTrust }
+       }
+
+       @Test
+       fun `negative trust can not be set to 101`() {
+               verifyThatWrongValueForPreferenceIsDetected("negative-trust", "101")
+       }
+
+       @Test
+       fun `negative trust is set to default on invalid value`() {
+               verifyThatPreferencesCanBeSet("negative-trust", "invalid", -25) { core.preferences.negativeTrust }
+       }
+
+       @Test
+       fun `positive trust can not be set to -1`() {
+               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "-1")
+       }
+
+       @Test
+       fun `positive trust can be set to 0`() {
+               verifyThatPreferencesCanBeSet("positive-trust", "0", 0) { core.preferences.positiveTrust }
+       }
+
+       @Test
+       fun `positive trust can be set to 100`() {
+               verifyThatPreferencesCanBeSet("positive-trust", "100", 100) { core.preferences.positiveTrust }
+       }
+
+       @Test
+       fun `positive trust can not be set to 101`() {
+               verifyThatWrongValueForPreferenceIsDetected("positive-trust", "101")
+       }
+
+       @Test
+       fun `positive trust is set to default on invalid value`() {
+               verifyThatPreferencesCanBeSet("positive-trust", "invalid", 75) { core.preferences.positiveTrust }
+       }
+
+       @Test
+       fun `post cut off length can not be set to -49`() {
+               verifyThatWrongValueForPreferenceIsDetected("post-cut-off-length", "-49")
+       }
+
+       @Test
+       fun `post cut off length can be set to 50`() {
+               verifyThatPreferencesCanBeSet("post-cut-off-length", "50", 50) { core.preferences.postCutOffLength }
+       }
+
+       @Test
+       fun `post cut off length is set to default on invalid value`() {
+               verifyThatPreferencesCanBeSet("post-cut-off-length", "invalid", 200) { core.preferences.postCutOffLength }
+       }
+
+       @Test
+       fun `posts per page can not be set to 0`() {
+               verifyThatWrongValueForPreferenceIsDetected("posts-per-page", "-49")
+       }
+
+       @Test
+       fun `posts per page can be set to 1`() {
+               verifyThatPreferencesCanBeSet("posts-per-page", "1", 1) { core.preferences.postsPerPage }
+       }
+
+       @Test
+       fun `posts per page is set to default on invalid value`() {
+               verifyThatPreferencesCanBeSet("posts-per-page", "invalid", 10) { core.preferences.postsPerPage }
+       }
+
+       @Test
+       fun `trust comment can be set`() {
+               verifyThatPreferencesCanBeSet("trust-comment", "trust", "trust") { core.preferences.trustComment }
+       }
+
+       @Test
+       fun `trust comment is set to default when set to empty value`() {
+               verifyThatPreferencesCanBeSet("trust-comment", "", "Set from Sone Web Interface") { core.preferences.trustComment }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ReloadingPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ReloadingPageTest.kt
new file mode 100644 (file)
index 0000000..2d44010
--- /dev/null
@@ -0,0 +1,51 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.pages.ReloadingPage
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.web.Method.GET
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.nio.file.Files
+import java.nio.file.Paths
+import kotlin.text.Charsets.UTF_8
+
+/**
+ * Unit test for [ReloadingPage].
+ */
+class ReloadingPageTest : WebPageTest() {
+
+       @Rule @JvmField val tempFolder = TemporaryFolder()
+       private val folder by lazy { tempFolder.newFolder() }
+       private val page by lazy { ReloadingPage<FreenetRequest>("/prefix/", folder.path, "text/plain") }
+
+       @Test
+       fun `page returns correct path prefix`() {
+               assertThat(page.path, equalTo("/prefix/"))
+       }
+
+       @Test
+       fun `page returns that it’s a prefix page`() {
+               assertThat(page.isPrefixPage, equalTo(true))
+       }
+
+       @Test
+       fun `requesting invalid file results in 404`() {
+               request("/prefix/path/file.txt", GET)
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(404))
+       }
+
+       @Test
+       fun `requesting valid file results in 200 and delivers file`() {
+               Files.write(Paths.get(folder.path, "file.txt"), listOf("Hello", "World"), UTF_8)
+               request("/prefix/path/file.txt", GET)
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.contentType, equalTo("text/plain"))
+               assertThat(responseBytes, equalTo("Hello\nWorld\n".toByteArray()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/RescuePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/RescuePageTest.kt
new file mode 100644 (file)
index 0000000..db6903e
--- /dev/null
@@ -0,0 +1,67 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.core.SoneRescuer
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.RescuePage
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [RescuePage].
+ */
+class RescuePageTest : WebPageTest() {
+
+       private val page = RescuePage(template, webInterface)
+
+       private val soneRescuer = mock<SoneRescuer>()
+
+       override fun getPage() = page
+
+       @Before
+       fun setupSoneRescuer() {
+               whenever(core.getSoneRescuer(currentSone)).thenReturn(soneRescuer)
+       }
+
+       @Test
+       fun `get request sets rescuer in template context`() {
+               request("", GET)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["soneRescuer"], equalTo<Any>(soneRescuer))
+       }
+
+       @Test
+       fun `post request redirects to rescue page`() {
+               request("", POST)
+               verifyRedirect("rescue.html")
+       }
+
+       @Test
+       fun `post request with fetch and invalid edition starts next fetch`() {
+               request("", POST)
+               addHttpRequestParameter("fetch", "true")
+               verifyRedirect("rescue.html") {
+                       verify(soneRescuer, never()).setEdition(anyLong())
+                       verify(soneRescuer).startNextFetch()
+               }
+       }
+
+       @Test
+       fun `post request with fetch and valid edition sets edition and starts next fetch`() {
+               request("", POST)
+               addHttpRequestParameter("fetch", "true")
+               addHttpRequestParameter("edition", "123")
+               verifyRedirect("rescue.html") {
+                       verify(soneRescuer).setEdition(123L)
+                       verify(soneRescuer).startNextFetch()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/SearchPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/SearchPageTest.kt
new file mode 100644 (file)
index 0000000..65b65a9
--- /dev/null
@@ -0,0 +1,257 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Optional.absent
+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.Sone
+import net.pterodactylus.sone.test.asOptional
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.SearchPage
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Test
+
+/**
+ * Unit test for [SearchPage].
+ */
+class SearchPageTest : WebPageTest() {
+
+       private val page = SearchPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `empty query redirects to index page`() {
+               verifyRedirect("index.html")
+       }
+
+       @Test
+       fun `empty search phrases redirect to index page`() {
+               addHttpRequestParameter("query", "\"\"")
+               verifyRedirect("index.html")
+       }
+
+       @Test
+       fun `invalid search phrases redirect to index page`() {
+               addHttpRequestParameter("query", "\"")
+               verifyRedirect("index.html")
+       }
+
+       @Test
+       fun `searching for sone link redirects to view sone page`() {
+               addSone("sone-id", mock<Sone>())
+               addHttpRequestParameter("query", "sone://sone-id")
+               verifyRedirect("viewSone.html?sone=sone-id")
+       }
+
+       @Test
+       fun `searching for sone link without prefix redirects to view sone page`() {
+               addSone("sone-id", mock<Sone>())
+               addHttpRequestParameter("query", "sone-id")
+               verifyRedirect("viewSone.html?sone=sone-id")
+       }
+
+       @Test
+       fun `searching for a post link redirects to post page`() {
+               addPost("post-id", mock<Post>())
+               addHttpRequestParameter("query", "post://post-id")
+               verifyRedirect("viewPost.html?post=post-id")
+       }
+
+       @Test
+       fun `searching for a post ID without prefix redirects to post page`() {
+               addPost("post-id", mock<Post>())
+               addHttpRequestParameter("query", "post-id")
+               verifyRedirect("viewPost.html?post=post-id")
+       }
+
+       @Test
+       fun `searching for a reply link redirects to the post page`() {
+               val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
+               addPostReply("reply-id", postReply)
+               addHttpRequestParameter("query", "reply://reply-id")
+               verifyRedirect("viewPost.html?post=post-id")
+       }
+
+       @Test
+       fun `searching for a reply ID redirects to the post page`() {
+               val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
+               addPostReply("reply-id", postReply)
+               addHttpRequestParameter("query", "reply-id")
+               verifyRedirect("viewPost.html?post=post-id")
+       }
+
+       @Test
+       fun `searching for an album link redirects to the image browser`() {
+               addAlbum("album-id", mock<Album>())
+               addHttpRequestParameter("query", "album://album-id")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `searching for an album ID redirects to the image browser`() {
+               addAlbum("album-id", mock<Album>())
+               addHttpRequestParameter("query", "album-id")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `searching for an image link redirects to the image browser`() {
+               addImage("image-id", mock<Image>())
+               addHttpRequestParameter("query", "image://image-id")
+               verifyRedirect("imageBrowser.html?image=image-id")
+       }
+
+       @Test
+       fun `searching for an image ID redirects to the image browser`() {
+               addImage("image-id", mock<Image>())
+               addHttpRequestParameter("query", "image-id")
+               verifyRedirect("imageBrowser.html?image=image-id")
+       }
+
+       private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
+               whenever(this.text).thenReturn(text)
+               postId?.run { whenever(this@apply.postId).thenReturn(postId) }
+               sone?.run { whenever(this@apply.sone).thenReturn(sone) }
+       }
+
+       private fun createPost(id: String, text: String) = mock<Post>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(recipient).thenReturn(absent())
+               whenever(this.text).thenReturn(text)
+       }
+
+       private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
+               whenever(posts).thenReturn(listOf(post))
+       } ?: mock<Sone>().apply {
+               whenever(posts).thenReturn(listOf(post))
+               whenever(profile).thenReturn(Profile(this))
+       }
+
+       @Test
+       fun `searching for a single word finds the post`() {
+               val postWithMatch = createPost("post-with-match", "the word here")
+               val postWithoutMatch = createPost("post-without-match", "no match here")
+               val soneWithMatch = createSoneWithPost(postWithMatch)
+               val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
+               addSone("sone-with-match", soneWithMatch)
+               addSone("sone-without-match", soneWithoutMatch)
+               addHttpRequestParameter("query", "word")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithMatch))
+       }
+
+       @Test
+       fun `searching for a single word locates word in reply`() {
+               val postWithMatch = createPost("post-with-match", "no match here")
+               val postWithoutMatch = createPost("post-without-match", "no match here")
+               val soneWithMatch = createSoneWithPost(postWithMatch)
+               val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
+               val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
+               val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
+               addPostReply("reply-with-match", replyWithMatch)
+               addPostReply("reply-without-match", replyWithoutMatch)
+               addSone("sone-with-match", soneWithMatch)
+               addSone("sone-without-match", soneWithoutMatch)
+               addHttpRequestParameter("query", "word")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithMatch))
+       }
+
+       private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
+                       createPost("post-$idPostfix", text, recipient).apply {
+                               addSone("sone-$idPostfix", createSoneWithPost(this, sender))
+                       }
+
+       @Test
+       fun `earlier matches score higher than later matches`() {
+               val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
+               val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
+               addHttpRequestParameter("query", "optional ")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
+       }
+
+       @Test
+       fun `searching for required word does not return posts without that word`() {
+               val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
+               createPost("without-required-match", "not a match")
+               addHttpRequestParameter("query", "+required ")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
+       }
+
+       @Test
+       fun `searching for forbidden word does not return posts with that word`() {
+               createSoneWithPost("with-forbidden-match", "forbidden match")
+               val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
+               addHttpRequestParameter("query", "match -forbidden")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
+       }
+
+       @Test
+       fun `searching for a plus sign searches for optional plus sign`() {
+               val postWithMatch = createSoneWithPost("with-match", "with + match")
+               createSoneWithPost("without-match", "without match")
+               addHttpRequestParameter("query", "+")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithMatch))
+       }
+
+       @Test
+       fun `searching for a minus sign searches for optional minus sign`() {
+               val postWithMatch = createSoneWithPost("with-match", "with - match")
+               createSoneWithPost("without-match", "without match")
+               addHttpRequestParameter("query", "-")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithMatch))
+       }
+
+       private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
+               whenever(this.id).thenReturn(id)
+               val recipientId = recipient?.id
+               whenever(this.recipientId).thenReturn(recipientId.asOptional())
+               whenever(this.recipient).thenReturn(recipient.asOptional())
+               whenever(this.text).thenReturn(text)
+       }
+
+       private fun createSone(id: String, firstName: String, middleName: String, lastName: String) = mock<Sone>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.name).thenReturn(id)
+               whenever(this.profile).thenReturn(Profile(this).apply {
+                       this.firstName = firstName
+                       this.middleName = middleName
+                       this.lastName = lastName
+               })
+       }
+
+       @Test
+       fun `searching for a recipient finds the correct post`() {
+               val recipient = createSone("recipient", "reci", "pi", "ent")
+               val postWithMatch = createSoneWithPost("with-match", "test", recipient)
+               createSoneWithPost("without-match", "no match")
+               addHttpRequestParameter("query", "recipient")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["postHits"], contains<Post>(postWithMatch))
+       }
+
+       @Test
+       fun `searching for a field value finds the correct sone`() {
+               val soneWithProfileField = createSone("sone", "s", "o", "ne")
+               soneWithProfileField.profile.addField("field").value = "value"
+               createSoneWithPost("with-match", "test", sender = soneWithProfileField)
+               createSoneWithPost("without-match", "no match")
+               addHttpRequestParameter("query", "value")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(this["soneHits"], contains(soneWithProfileField))
+       }
+
+       @Suppress("UNCHECKED_CAST")
+       private operator fun <T> get(key: String): T? = templateContext[key] as? T
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePageTest.kt
new file mode 100644 (file)
index 0000000..42d6676
--- /dev/null
@@ -0,0 +1,269 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.main.SonePlugin
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.SoneTemplatePage
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.version.Version
+import net.pterodactylus.util.web.Method.GET
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.anyOf
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [SoneTemplatePage].
+ */
+class SoneTemplatePageTest : WebPageTest() {
+
+       private val preferences by lazy { core.preferences!! }
+       private val page = object : SoneTemplatePage("path.html", template, webInterface, true) {}
+
+       @Test
+       fun `current sone is retrieved from web interface`() {
+               assertThat(page.getCurrentSone(toadletContext), equalTo(currentSone))
+       }
+
+       @Test
+       fun `retrieving current sone without creation is forwarded to web interface`() {
+               mock<Sone>().let {
+                       whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(it)
+                       assertThat(page.getCurrentSoneWithoutCreatingSession(toadletContext), equalTo(it))
+               }
+       }
+
+       @Test
+       fun `setting the current sone is forwarded to web interface`() {
+               mock<Sone>().let {
+                       page.setCurrentSone(toadletContext, it)
+                       verify(webInterface).setCurrentSone(toadletContext, it)
+               }
+       }
+
+       @Test
+       fun `page title is empty string if no page title key was given`() {
+               SoneTemplatePage("path.html", template, null, webInterface).let { page ->
+                       assertThat(page.getPageTitle(freenetRequest), equalTo(""))
+               }
+       }
+
+       @Test
+       fun `page title is retrieved from l10n if page title key is given`() {
+               SoneTemplatePage("path.html", template, "page.title", webInterface).let { page ->
+                       whenever(l10n.getString("page.title")).thenReturn("Page Title")
+                       assertThat(page.getPageTitle(freenetRequest), equalTo("Page Title"))
+               }
+       }
+
+       @Test
+       fun `additional link nodes contain open search link`() {
+               addHttpRequestHeader("Host", "www.example.com")
+               assertThat(page.getAdditionalLinkNodes(freenetRequest), contains(mapOf(
+                               "rel" to "search",
+                               "type" to "application/opensearchdescription+xml",
+                               "title" to "Sone",
+                               "href" to "http://www.example.com/Sone/OpenSearch.xml"
+               )))
+       }
+
+       @Test
+       fun `style sheets contains sone CSS file`() {
+               assertThat(page.styleSheets, contains("css/sone.css"))
+       }
+
+       @Test
+       fun `shortcut icon is the sone icon`() {
+               assertThat(page.shortcutIcon, equalTo("images/icon.png"))
+       }
+
+       @Test
+       fun `page requires login if require login was specified in the constructor`() {
+               SoneTemplatePage("path.html", template, webInterface, true).let { page ->
+                       assertThat(page.requiresLogin(), equalTo(true))
+               }
+       }
+
+       @Test
+       fun `page does not require login if require login was not specified in the constructor`() {
+               SoneTemplatePage("path.html", template, webInterface, false).let { page ->
+                       assertThat(page.requiresLogin(), equalTo(false))
+               }
+       }
+
+       private fun verifyVariableIsSet(name: String, value: Any) = verifyVariableMatches(name, equalTo<Any>(value))
+
+       private fun <T> verifyVariableMatches(name: String, matcher: Matcher<T>) {
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext[name] as T, matcher)
+       }
+
+       @Test
+       fun `preferences are set in template context`() {
+           verifyVariableIsSet("preferences", preferences)
+       }
+
+       @Test
+       fun `current sone is set in template context`() {
+               verifyVariableIsSet("currentSone", currentSone)
+       }
+
+       @Test
+       fun `local sones are set in template context`() {
+               val localSones = listOf(mock<Sone>(), mock<Sone>())
+               whenever(core.localSones).thenReturn(localSones)
+               verifyVariableMatches("localSones", containsInAnyOrder(*localSones.toTypedArray()))
+       }
+
+       @Test
+       fun `freenet request is set in template context`() {
+               verifyVariableIsSet("request", freenetRequest)
+       }
+
+       @Test
+       fun `current version is set in template context`() {
+               verifyVariableIsSet("currentVersion", SonePlugin.getPluginVersion())
+       }
+
+       @Test
+       fun `has latest version is set correctly in template context if true`() {
+               whenever(core.updateChecker.hasLatestVersion()).thenReturn(true)
+               verifyVariableIsSet("hasLatestVersion", true)
+       }
+
+       @Test
+       fun `has latest version is set correctly in template context if false`() {
+               whenever(core.updateChecker.hasLatestVersion()).thenReturn(false)
+               verifyVariableIsSet("hasLatestVersion", false)
+       }
+
+       @Test
+       fun `latest edition is set in template context`() {
+               whenever(core.updateChecker.latestEdition).thenReturn(1234L)
+               verifyVariableIsSet("latestEdition", 1234L)
+       }
+
+       @Test
+       fun `latest version is set in template context`() {
+               whenever(core.updateChecker.latestVersion).thenReturn(Version(1, 2, 3))
+               verifyVariableIsSet("latestVersion", Version(1, 2, 3))
+       }
+
+       @Test
+       fun `latest version time is set in template context`() {
+               whenever(core.updateChecker.latestVersionDate).thenReturn(12345L)
+               verifyVariableIsSet("latestVersionTime", 12345L)
+       }
+
+       private fun createNotification(time: Long) = mock<Notification>().apply {
+               whenever(createdTime).thenReturn(time)
+       }
+
+       @Test
+       fun `notifications are set in template context`() {
+               val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000))
+               whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications)
+               verifyVariableMatches("notifications", contains(notifications[1], notifications[2], notifications[0]))
+       }
+
+       @Test
+       fun `notification hash is set in template context`() {
+               val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000))
+               whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications)
+               verifyVariableIsSet("notificationHash", listOf(notifications[1], notifications[2], notifications[0]).hashCode())
+       }
+
+       @Test
+       fun `handleRequest method is called`() {
+               var called = false
+               val page = object : SoneTemplatePage("path.html", template, webInterface, true) {
+                       override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) {
+                               called = true
+                       }
+               }
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(called, equalTo(true))
+       }
+
+       @Test
+       fun `redirect does not happen if login is not required`() {
+               val page = SoneTemplatePage("page.html", template, webInterface, false)
+               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
+       }
+
+       @Test
+       fun `redirect does not happen if sone is logged in`() {
+               assertThat(page.getRedirectTarget(freenetRequest), nullValue())
+       }
+
+       @Test
+       fun `redirect does happen if sone is not logged in`() {
+               unsetCurrentSone()
+               request("index.html", GET)
+               assertThat(page.getRedirectTarget(freenetRequest), equalTo("login.html?target=index.html"))
+       }
+
+       @Test
+       fun `redirect does happen with parameters encoded correctly if sone is not logged in`() {
+               unsetCurrentSone()
+               request("index.html", GET)
+               addHttpRequestParameter("foo", "b=r")
+               addHttpRequestParameter("baz", "q&o")
+               assertThat(page.getRedirectTarget(freenetRequest), anyOf(
+                               equalTo("login.html?target=index.html%3Ffoo%3Db%253Dr%26baz%3Dq%2526o"),
+                               equalTo("login.html?target=index.html%3Fbaz%3Dq%2526o%26foo%3Db%253Dr")
+               ))
+       }
+
+       @Test
+       fun `full access requirement is correctly forwarded from the preferences if false`() {
+               assertThat(page.isFullAccessOnly, equalTo(false))
+       }
+
+       @Test
+       fun `full access requirement is correctly forwarded from the preferences if true`() {
+               core.preferences.isRequireFullAccess = true
+               assertThat(page.isFullAccessOnly, equalTo(true))
+       }
+
+       @Test
+       fun `page is disabled if full access is required but request does not have full access`() {
+               core.preferences.isRequireFullAccess = true
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is disabled if login is required but there is no current sone`() {
+               unsetCurrentSone()
+               assertThat(page.isEnabled(toadletContext), equalTo(false))
+       }
+
+       @Test
+       fun `page is enabled if login is required and there is a current sone`() {
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `page is enabled if full access is required and request has full access and login is required and there is a current sone`() {
+               core.preferences.isRequireFullAccess = true
+               whenever(toadletContext.isAllowedFullAccess).thenReturn(true)
+               assertThat(page.isEnabled(toadletContext), equalTo(true))
+       }
+
+       @Test
+       fun `page is enabled if no full access is required and login is not required`() {
+               SoneTemplatePage("path.html", template, webInterface, false).let { page ->
+                       assertThat(page.isEnabled(toadletContext), equalTo(true))
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/TrustPageTest.kt
new file mode 100644 (file)
index 0000000..6947f63
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.TrustPage
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [TrustPage].
+ */
+class TrustPageTest : WebPageTest() {
+
+       private val page = TrustPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get method does not redirect`() {
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with missing sone redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "sone-id")
+               verifyRedirect("return.html") {
+                       verify(core, never()).trustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request with existing sone trusts the identity and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "sone-id")
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               verifyRedirect("return.html") {
+                       verify(core).trustSone(eq(currentSone), eq(sone))
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPageTest.kt
new file mode 100644 (file)
index 0000000..b18211f
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.UnbookmarkPage
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnbookmarkPage].
+ */
+class UnbookmarkPageTest : WebPageTest() {
+
+       private val page = UnbookmarkPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `get request with all-not-loaded parameter unloads all not loaded posts and redirects to bookmarks`() {
+               addHttpRequestParameter("post", "allNotLoaded")
+               val loadedPost1 = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
+               val loadedPost2 = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
+               val notLoadedPost1 = mock<Post>()
+               val notLoadedPost2 = mock<Post>()
+               whenever(core.bookmarkedPosts).thenReturn(setOf(loadedPost1, loadedPost2, notLoadedPost1, notLoadedPost2))
+               verifyRedirect("bookmarks.html") {
+                       verify(core).unbookmarkPost(notLoadedPost1)
+                       verify(core).unbookmarkPost(notLoadedPost2)
+                       verify(core, never()).unbookmarkPost(loadedPost1)
+                       verify(core, never()).unbookmarkPost(loadedPost2)
+               }
+       }
+
+       @Test
+       fun `post request does not unbookmark not-present post but redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unbookmarkPost(any())
+               }
+       }
+
+       @Test
+       fun `post request unbookmarks present post and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("returnPage", "return.html")
+               val post = mock<Post>().apply { whenever(isLoaded).thenReturn(true) }
+               addPost("post-id", post)
+               verifyRedirect("return.html") {
+                       verify(core).unbookmarkPost(post)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePageTest.kt
new file mode 100644 (file)
index 0000000..5bcd65f
--- /dev/null
@@ -0,0 +1,43 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.pages.UnfollowSonePage
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnfollowSonePage].
+ */
+class UnfollowSonePageTest : WebPageTest() {
+
+       private val page = UnfollowSonePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request unfollows a single sone and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "sone-id")
+               verifyRedirect("return.html") {
+                       verify(core).unfollowSone(currentSone, "sone-id")
+               }
+       }
+
+       @Test
+       fun `post request unfollows two sones and redirects to return page`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "sone-id1, sone-id2")
+               verifyRedirect("return.html") {
+                       verify(core).unfollowSone(currentSone, "sone-id1")
+                       verify(core).unfollowSone(currentSone, "sone-id2")
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UnlikePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UnlikePageTest.kt
new file mode 100644 (file)
index 0000000..0de0308
--- /dev/null
@@ -0,0 +1,61 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.pages.UnlikePage
+import net.pterodactylus.util.web.Method
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnlikePage].
+ */
+class UnlikePageTest : WebPageTest() {
+
+       private val page = UnlikePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               page.handleRequest(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request does not remove any likes but redirects`() {
+           request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(currentSone, never()).removeLikedPostId(any())
+                       verify(currentSone, never()).removeLikedReplyId(any())
+               }
+       }
+
+       @Test
+       fun `post request removes post like and redirects`() {
+           request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("type", "post")
+               addHttpRequestParameter("id", "post-id")
+               verifyRedirect("return.html") {
+                       verify(currentSone, never()).removeLikedPostId("post-id")
+                       verify(currentSone, never()).removeLikedReplyId(any())
+               }
+       }
+
+       @Test
+       fun `post request removes reply like and redirects`() {
+           request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("type", "reply")
+               addHttpRequestParameter("id", "reply-id")
+               verifyRedirect("return.html") {
+                       verify(currentSone, never()).removeLikedPostId(any())
+                       verify(currentSone, never()).removeLikedReplyId("reply-id")
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePageTest.kt
new file mode 100644 (file)
index 0000000..390c1fc
--- /dev/null
@@ -0,0 +1,59 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.UnlockSonePage
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnlockSonePage].
+ */
+class UnlockSonePageTest : WebPageTest() {
+
+       private val page = UnlockSonePage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request without sone redirects to return page`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `get request without invalid local sone does not unlock any sone and redirects to return page`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "invalid-sone")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `get request without remote sone does not unlock any sone and redirects to return page`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "remote-sone")
+               addSone("remote-sone", mock<Sone>())
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `get request with local sone unlocks sone and redirects to return page`() {
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "local-sone")
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               addLocalSone("local-sone", sone)
+               verifyRedirect("return.html") {
+                       verify(core).unlockSone(sone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UntrustPageTest.kt
new file mode 100644 (file)
index 0000000..0a59453
--- /dev/null
@@ -0,0 +1,59 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.pages.UntrustPage
+import net.pterodactylus.util.web.Method.POST
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UntrustPage].
+ */
+class UntrustPageTest : WebPageTest() {
+
+       private val page = UntrustPage(template, webInterface)
+
+       override fun getPage() = page
+
+       @Test
+       fun `get request does not redirect`() {
+               page.handleRequest(freenetRequest, templateContext)
+               verify(core, never()).untrustSone(eq(currentSone), any())
+       }
+
+       @Test
+       fun `post request without sone parameter does not untrust but redirects`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).untrustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request with invalid sone parameter does not untrust but redirects`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "no-sone")
+               verifyRedirect("return.html") {
+                       verify(core, never()).untrustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request with valid sone parameter untrusts and redirects`() {
+               request("", POST)
+               addHttpRequestParameter("returnPage", "return.html")
+               addHttpRequestParameter("sone", "sone-id")
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               verifyRedirect("return.html") {
+                       verify(core).untrustSone(currentSone, sone)
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/UploadImagePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/UploadImagePageTest.kt
new file mode 100644 (file)
index 0000000..232601b
--- /dev/null
@@ -0,0 +1,101 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Image.Modifier
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.TemporaryImage
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.mockBuilder
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.UploadImagePage
+import net.pterodactylus.util.web.Method.POST
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UploadImagePage].
+ */
+class UploadImagePageTest : WebPageTest() {
+
+       private val parentAlbum = mock<Album>().apply {
+               whenever(id).thenReturn("parent-id")
+               whenever(sone).thenReturn(currentSone)
+       }
+
+       override fun getPage() = UploadImagePage(template, webInterface)
+
+       @Test
+       fun `get request does not redirect or upload anything`() {
+               page.handleRequest(freenetRequest, templateContext)
+               verify(core, never()).createTemporaryImage(any(), any())
+               verify(core, never()).createImage(any(), any(), any())
+       }
+
+       @Test
+       fun `post request without parent results in no permission error page`() {
+               request("", POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with parent that is not the current sone results in no permission error page`() {
+               request("", POST)
+               addHttpRequestParameter("parent", "parent-id")
+               whenever(parentAlbum.sone).thenReturn(mock<Sone>())
+               addAlbum("parent-id", parentAlbum)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with empty name redirects to error page`() {
+               request("", POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestParameter("parent", "parent-id")
+               addHttpRequestParameter("title", " ")
+               verifyRedirect("emptyImageTitle.html")
+       }
+
+       @Test
+       fun `uploading an invalid image results in no redirect and message set in template context`() {
+               request("", POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestParameter("parent", "parent-id")
+               addHttpRequestParameter("title", "title")
+               addUploadedFile("image", "image.png", "image/png", "no-image.png")
+               page.handleRequest(freenetRequest, templateContext)
+               verify(core, never()).createTemporaryImage(any(), any())
+               assertThat(templateContext["messages"] as String?, equalTo<String>("Page.UploadImage.Error.InvalidImage"))
+       }
+
+       @Test
+       fun `uploading a valid image uploads image and redirects to album browser`() {
+               request("", POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestParameter("parent", "parent-id")
+               addHttpRequestParameter("title", "Title")
+               addHttpRequestParameter("description", "Description")
+               addUploadedFile("image", "image.png", "image/png", "image.png")
+               val temporaryImage = TemporaryImage("temp-image")
+               val imageModifier = mockBuilder<Modifier>()
+               val image = mock<Image>().apply {
+                       whenever(modify()).thenReturn(imageModifier)
+               }
+               whenever(core.createTemporaryImage(eq("image/png"), any())).thenReturn(temporaryImage)
+               whenever(core.createImage(currentSone, parentAlbum, temporaryImage)).thenReturn(image)
+               verifyRedirect("imageBrowser.html?album=parent-id") {
+                       verify(image).modify()
+                       verify(imageModifier).setWidth(2)
+                       verify(imageModifier).setHeight(1)
+                       verify(imageModifier).setTitle("Title")
+                       verify(imageModifier).setDescription("Description")
+                       verify(imageModifier).update()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ViewPostPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ViewPostPageTest.kt
new file mode 100644 (file)
index 0000000..bbc03bf
--- /dev/null
@@ -0,0 +1,87 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.ViewPostPage
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [ViewPostPage].
+ */
+class ViewPostPageTest : WebPageTest() {
+
+       private val page = ViewPostPage(template, webInterface)
+       private val post = mock<Post>()
+
+       override fun getPage() = page
+
+       @Test
+       fun `the view post page is link-excepted`() {
+               assertThat(page.isLinkExcepted(null), equalTo(true))
+       }
+
+       @Test
+       fun `get request without parameters stores null in template context`() {
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["post"], nullValue())
+               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
+       }
+
+       @Test
+       fun `get request with invalid post id stores null in template context`() {
+               addHttpRequestParameter("post", "invalid-post-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["post"], nullValue())
+               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
+       }
+
+       @Test
+       fun `get request with valid post id stores post in template context`() {
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["post"], equalTo<Any>(post))
+               assertThat(templateContext["raw"] as? Boolean, equalTo(false))
+       }
+
+       @Test
+       fun `get request with valid post id and raw=true stores post in template context`() {
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               addHttpRequestParameter("raw", "true")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["post"], equalTo<Any>(post))
+               assertThat(templateContext["raw"] as? Boolean, equalTo(true))
+       }
+
+       @Test
+       fun `page title for request without parameters is default title`() {
+               assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewPost.Title"))
+       }
+
+       @Test
+       fun `page title for request with invalid post is default title`() {
+               addHttpRequestParameter("post", "invalid-post-id")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewPost.Title"))
+       }
+
+       @Test
+       fun `page title for request with valid post is first twenty chars of post plus sone name plus default title`() {
+               whenever(currentSone.profile).thenReturn(Profile(currentSone).apply {
+                       firstName = "First"
+                       middleName = "M."
+                       lastName = "Last"
+               })
+               whenever(post.sone).thenReturn(currentSone)
+               whenever(post.text).thenReturn("This is a text that is longer than twenty characters.")
+               addPost("post-id", post)
+               addHttpRequestParameter("post", "post-id")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("This is a text that … - First M. Last - Page.ViewPost.Title"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/ViewSonePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/ViewSonePageTest.kt
new file mode 100644 (file)
index 0000000..641e73e
--- /dev/null
@@ -0,0 +1,156 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.asOptional
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.pages.ViewSonePage
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [ViewSonePage].
+ */
+class ViewSonePageTest : WebPageTest() {
+
+       init {
+               whenever(currentSone.id).thenReturn("sone-id")
+       }
+
+       private val page = ViewSonePage(template, webInterface)
+       private val post1 = createPost("post1", "First Post.", 1000, currentSone)
+       private val post2 = createPost("post2", "Second Post.", 2000, currentSone)
+       private val foreignPost1 = createPost("foreign-post1", "First Foreign Post.", 1000, mock<Sone>())
+       private val foreignPost2 = createPost("foreign-post2", "Second Foreign Post.", 2000, mock<Sone>())
+       private val directed1 = createPost("post3", "First directed.", 1500, mock<Sone>(), recipient = currentSone)
+       private val directed2 = createPost("post4", "Second directed.", 2500, mock<Sone>(), recipient = currentSone)
+
+       override fun getPage() = page
+
+       @Before
+       fun setup() {
+               whenever(currentSone.posts).thenReturn(mutableListOf(post2, post1))
+               whenever(core.getDirectedPosts("sone-id")).thenReturn(setOf(directed1, directed2))
+               core.preferences.postsPerPage = 2
+       }
+
+       @Test
+       fun `get request without sone parameter stores null in template context`() {
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["sone"], nullValue())
+               assertThat(templateContext["soneId"], equalTo<Any>(""))
+       }
+
+       @Test
+       fun `get request with invalid sone parameter stores null in template context`() {
+               addHttpRequestParameter("sone", "invalid-sone-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["sone"], nullValue())
+               assertThat(templateContext["soneId"], equalTo<Any>("invalid-sone-id"))
+       }
+
+       @Test
+       fun `get request with valid sone parameter stores sone in template context`() {
+               whenever(currentSone.posts).thenReturn(mutableListOf())
+               whenever(core.getDirectedPosts("sone-id")).thenReturn(emptyList())
+               addHttpRequestParameter("sone", "sone-id")
+               addSone("sone-id", currentSone)
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["sone"], equalTo<Any>(currentSone))
+               assertThat(templateContext["soneId"], equalTo<Any>("sone-id"))
+       }
+
+       private fun createPost(id: String, text: String, time: Long, sender: Sone? = null, recipient: Sone? = null) = mock<Post>().apply {
+               whenever(this.id).thenReturn(id)
+               sender?.run { whenever(this@apply.sone).thenReturn(this) }
+               val recipientId = recipient?.id
+               whenever(this.recipientId).thenReturn(recipientId.asOptional())
+               whenever(this.recipient).thenReturn(recipient.asOptional())
+               whenever(this.time).thenReturn(time)
+               whenever(this.text).thenReturn(text)
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `request with valid sone stores posts and directed posts in template context`() {
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(directed2, post2))
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `second page of posts is shown correctly`() {
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               addHttpRequestParameter("postPage", "1")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(directed1, post1))
+       }
+
+       private fun createReply(text: String, time: Long, post: Post?) = mock<PostReply>().apply {
+               whenever(this.text).thenReturn(text)
+               whenever(this.time).thenReturn(time)
+               whenever(this.post).thenReturn(post.asOptional())
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `replies are shown correctly`() {
+           val reply1 = createReply("First Reply", 1500, foreignPost1)
+               val reply2 = createReply("Second Reply", 2500, foreignPost2)
+               val reply3 = createReply("Third Reply", 1750, post1)
+               val reply4 = createReply("Fourth Reply", 2250, post2)
+           val reply5 = createReply("Fifth Reply", 1600, post1)
+           val reply6 = createReply("Sixth Reply", 2100, directed1)
+           val reply7 = createReply("Seventh Reply", 2200, null)
+           val reply8 = createReply("Eigth Reply", 2300, foreignPost1)
+               whenever(currentSone.replies).thenReturn(setOf(reply1, reply2, reply3, reply4, reply5, reply6, reply7, reply8))
+               whenever(core.getReplies("post1")).thenReturn(listOf(reply3, reply5))
+               whenever(core.getReplies("post2")).thenReturn(listOf(reply4))
+               whenever(core.getReplies("foreign-post1")).thenReturn(listOf(reply1))
+               whenever(core.getReplies("foreign-post2")).thenReturn(listOf(reply2))
+               whenever(core.getReplies("post3")).thenReturn(listOf(reply6))
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               page.handleRequest(freenetRequest, templateContext)
+               assertThat(templateContext["repliedPosts"] as Iterable<Post>, contains(foreignPost2,  foreignPost1))
+       }
+
+       @Test
+       fun `page title is default for request without parameters`() {
+           assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewSone.Page.TitleWithoutSone"))
+       }
+
+       @Test
+       fun `page title is default for request with invalid sone parameters`() {
+               addHttpRequestParameter("sone", "invalid-sone-id")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("Page.ViewSone.Page.TitleWithoutSone"))
+       }
+
+       @Test
+       fun `page title contains sone name for request with sone parameters`() {
+               addHttpRequestParameter("sone", "sone-id")
+               addSone("sone-id", currentSone)
+               whenever(currentSone.profile).thenReturn(Profile(currentSone).apply {
+                       firstName = "First"
+                       middleName = "M."
+                       lastName = "Last"
+               })
+           assertThat(page.getPageTitle(freenetRequest), equalTo("First M. Last - Page.ViewSone.Title"))
+       }
+
+       @Test
+       fun `page is link-excepted`() {
+           assertThat(page.isLinkExcepted(null), equalTo(true))
+       }
+
+}
diff --git a/src/test/resources/net/pterodactylus/sone/web/image.png b/src/test/resources/net/pterodactylus/sone/web/image.png
deleted file mode 100644 (file)
index 6daa837..0000000
Binary files a/src/test/resources/net/pterodactylus/sone/web/image.png and /dev/null differ
diff --git a/src/test/resources/net/pterodactylus/sone/web/no-image.png b/src/test/resources/net/pterodactylus/sone/web/no-image.png
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/test/resources/net/pterodactylus/sone/web/pages/image.png b/src/test/resources/net/pterodactylus/sone/web/pages/image.png
new file mode 100644 (file)
index 0000000..6daa837
Binary files /dev/null and b/src/test/resources/net/pterodactylus/sone/web/pages/image.png differ
diff --git a/src/test/resources/net/pterodactylus/sone/web/pages/no-image.png b/src/test/resources/net/pterodactylus/sone/web/pages/no-image.png
new file mode 100644 (file)
index 0000000..e69de29