Merge branch 'release-0.9.7' 0.9.7
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 7 Oct 2017 16:55:06 +0000 (18:55 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 7 Oct 2017 16:55:06 +0000 (18:55 +0200)
455 files changed:
.gitignore
README.md
build.gradle
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/SoneOptions.java
src/main/java/net/pterodactylus/sone/data/impl/IdOnlySone.java
src/main/java/net/pterodactylus/sone/data/impl/SoneImpl.java
src/main/java/net/pterodactylus/sone/database/SoneProvider.java
src/main/java/net/pterodactylus/sone/fcp/AbstractSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java
src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java
src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java
src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java
src/main/java/net/pterodactylus/sone/fcp/LockSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/UnlockSoneCommand.java
src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityChangeDetector.java
src/main/java/net/pterodactylus/sone/main/DebugLoaders.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/FilesystemTemplate.java
src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java [deleted file]
src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/template/SubstringFilter.java
src/main/java/net/pterodactylus/sone/template/TrustAccessor.java
src/main/java/net/pterodactylus/sone/text/FreemailPart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java [deleted file]
src/main/java/net/pterodactylus/sone/text/LinkPart.java [deleted file]
src/main/java/net/pterodactylus/sone/text/Part.java [deleted file]
src/main/java/net/pterodactylus/sone/text/PartContainer.java [deleted file]
src/main/java/net/pterodactylus/sone/text/PlainTextPart.java [deleted file]
src/main/java/net/pterodactylus/sone/text/SonePart.java [deleted file]
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java
src/main/java/net/pterodactylus/sone/utils/NumberParsers.java
src/main/java/net/pterodactylus/sone/web/AboutPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/DistrustPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java [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/LoginPage.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/ajax/BookmarkAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/JsonReturnObject.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/L10nText.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/NoArg.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/VersionParser.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/ParserFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/RenderFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/ShortenFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/FreenetLinkPart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/LinkPart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/Part.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/PlainTextPart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/SonePart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/TimeText.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/text/TimeTextConverter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Buckets.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Json.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Optionals.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Pagination.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Requests.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Strings.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Templates.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObject.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt [new file with mode: 0644]
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/EditImagePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/GetImagePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/IndexPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/LoginPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/NewPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/ReloadingPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/RescuePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/ViewPostPage.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt [new file with mode: 0644]
src/main/resources/i18n/sone.de.properties
src/main/resources/i18n/sone.en.properties
src/main/resources/i18n/sone.es.properties
src/main/resources/i18n/sone.fr.properties
src/main/resources/i18n/sone.ja.properties
src/main/resources/i18n/sone.no.properties
src/main/resources/i18n/sone.pl.properties
src/main/resources/i18n/sone.ru.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/imageBrowser.html
src/main/resources/templates/include/browseAlbums.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/include/viewSone.html
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html
src/main/resources/templates/linked/html-page.html [new file with mode: 0644]
src/main/resources/templates/linked/image.html [new file with mode: 0644]
src/main/resources/templates/linked/notLoaded.html [new file with mode: 0644]
src/main/resources/templates/notify/newVersionNotification.html
src/main/resources/templates/notify/soneInsertNotification.html
src/main/resources/templates/options.html
src/main/resources/templates/viewSone.html
src/test/java/net/pterodactylus/sone/Matchers.java [deleted file]
src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java [deleted file]
src/test/java/net/pterodactylus/sone/TestImageBuilder.java [deleted file]
src/test/java/net/pterodactylus/sone/TestPostBuilder.java [deleted file]
src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java [deleted file]
src/test/java/net/pterodactylus/sone/TestUtil.java [deleted file]
src/test/java/net/pterodactylus/sone/TestValue.java [deleted file]
src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java
src/test/java/net/pterodactylus/sone/core/FreenetInterfaceTest.java
src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java
src/test/java/net/pterodactylus/sone/database/memory/ConfigurationLoaderTest.java
src/test/java/net/pterodactylus/sone/database/memory/MemoryBookmarkDatabaseTest.java
src/test/java/net/pterodactylus/sone/database/memory/MemoryDatabaseTest.java
src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java [deleted file]
src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java [deleted file]
src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java [deleted file]
src/test/java/net/pterodactylus/sone/freenet/wot/DefaultIdentityTest.java
src/test/java/net/pterodactylus/sone/notify/ListNotificationTest.java
src/test/java/net/pterodactylus/sone/template/AlbumAccessorTest.java
src/test/java/net/pterodactylus/sone/template/FilesystemTemplateTest.java
src/test/java/net/pterodactylus/sone/template/IdentityAccessorTest.java
src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/Matchers.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestAlbumBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestImageBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestPostBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestPostReplyBuilder.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestUtil.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/test/TestValue.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/text/FreemailPartTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/text/FreenetLinkPartTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/LinkPartTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/PartContainerTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/PlainTextPartTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/SonePartTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java
src/test/java/net/pterodactylus/sone/utils/IntegerRangePredicateTest.java
src/test/java/net/pterodactylus/sone/web/AboutPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/BookmarkPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/BookmarksPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/CreatePostPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/CreateSonePageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/DeleteReplyPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/NewPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/UploadImagePageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/WebPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java [deleted file]
src/test/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.java [deleted file]
src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/CreateReplyCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/DeletePostCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/DeleteReplyCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/FcpInterfaceTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetLocalSonesCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetPostCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetPostFeedCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetPostsCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetSoneCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/GetSonesCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/LikePostCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/LikeReplyCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/LockSoneCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/SoneCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/fcp/VersionCommandTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/VersionParserTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ImageAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/RenderFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ReplyAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ReplyGroupFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/RequestChangeFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/SoneAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/SubstringFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/TrustAccessorTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/UniqueElementFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/Guice.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/OneByOneMatcher.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/PaginationMatcher.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/text/TimeTextConverterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/BucketsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/JsonTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/OptionalsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/PaginationTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/RequestsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/utils/StringsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObjectTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageBaseTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObjectTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPageTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt [new file with mode: 0644]
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/NewPageTest.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/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt [new file with mode: 0644]
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/element-loader.html [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/element-loader2.html [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/element-loader3.html [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/core/element-loader4.html [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/main/custom-version.yaml [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/web/pages/upload-image-invalid-image.png [new file with mode: 0644]
src/test/resources/net/pterodactylus/sone/web/pages/upload-image-value-image.png [new file with mode: 0644]
src/test/resources/version.yaml [new file with mode: 0644]
version.gradle [new file with mode: 0644]

index ea8c4bf..99b31c6 100644 (file)
@@ -1 +1,2 @@
 /target
+/src/generated
index c8e8a00..f3a7df1 100644 (file)
--- a/README.md
+++ b/README.md
@@ -2,19 +2,31 @@
 
 Sone aims to provide social network functionality for [Freenet](https://freenetproject.org/) (also here [on GitHub](https://github.com/freenet/)).
 
+## Compiling
+
+Sone’s build process is handled by [Gradle](https://gradle.org/). Just use the Gradle wrapper that comes with Sone:
+
+    # ./gradlew clean build fatJar
+
+This will resolve Sone’s dependencies, compile Sone, run all the tests, and put the file `sone-jar-with-dependencies.jar` into the `build/libs` directory. This is the file that you can load from Freenet’s plugin manager to run Sone.
+
 ## Installing
 
 ### Prerequisites
 
 For Sone to work you will need a running Freenet node, of course. You will also need the Web of Trust plugin from the official plugins listed on your node’s plugin manager page (*Configuration → Plugins* in the menu).
 
-If you already have a web of trust identity, you can skip the next section.
-
-You will also need to create an identity in the web of trust. Select *Community* from the menu, choose “Generate” and follow the instructions on-screen until your identity has been created.
+You will also need a web of trust identity to use Sone. If you do not already have a web of trust identity, select *Community* from the menu, choose “Generate” and follow the instructions on-screen until your identity has been created.
 
 ### Loading/Installing
 
-For Sone to work you will need a running Freenet node and the WebOfTrust plugin. Loading Sone happens from Freenet’s web interface using the “Add an Unofficial Plugin from Freenet” section from the node’s plugin manager at *Configuration → Plugins*, at the bottom of the page. Enter the key of the plugin (starting with “USK@”) into the text field and press the “Load” button. The plugin should then be downloaded from Freenet and started once it’s ready.
+#### From Freenet
+
+If you just want to run the latest version of Sone you first have to obtain the plugin key from [Sone’s homepage in Freenet](USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/75/). Now head over to your node’s plugin manager which you can reach from the menu at *Configuration → Plugins*. At the bottom of the page there’s a section called “Add an Unofficial Plugin from Freenet.” Enter the key of the plugin into the text field and press the “Load” button. The plugin should then be downloaded from Freenet and started once it’s ready.
+
+#### From Disk
+
+If you have compiled Sone yourself, there’s a different section on the plugin manager page, called “Add an Unofficial Plugin.” Enter the full path name of the JAR file created by Gradle and press the “Load” button. Sone should then be ready instantly.
 
 The node will remember which plugins you loaded so that you don’t need to do that again after your node restarts (e.g. for updates).
 
@@ -48,6 +60,6 @@ Now, a social network wouldn’t be much fun if you couldn’t talk with other p
 
 When displaying posts and replies, Sone first parses the text. Special elements, such as Freenet URIs and Sone elements with a special syntax, are replaced with formatting that allow your browser to navigate the elements. Sone recognizes the following elements:
 
-* Links to Freenet URIs are linked to as-is. Make sure to separate the URI from surrounding text by whitespace, such as space or line breaks.
+* Links to Freenet URIs are linked to as-is. Make sure to separate the URI from surrounding text by whitespace, such as space or line breaks, or interpunction, such as commas and periods.
 * Links to other Sone’s profiles are added by the prefix “sone://” followed by the ID of the Sone. It is also possible to get the link for a Sone from a post or reply by that Sone; just copy the URL behind the “[link author]” link.
 * Links to other posts are added by the prefix “post://” followed by the ID of the post. You can also find the post ID behind the “[link post]” link below a post.
index 9221dd3..ecdbfdd 100644 (file)
@@ -1,12 +1,15 @@
 group = 'net.pterodactylus'
-version = '0.9.6'
+version = '0.9.7'
 
 buildscript {
+    ext.kotlinVersion = '1.1.51'
     repositories {
         mavenCentral()
     }
     dependencies {
-        classpath group: 'info.solidsoft.gradle.pitest', name: 'gradle-pitest-plugin', version: '1.1.10'
+        classpath group: 'info.solidsoft.gradle.pitest', name: 'gradle-pitest-plugin', version: '1.1.11'
+        classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: kotlinVersion
+        classpath group: 'org.jetbrains.kotlin', name: 'kotlin-noarg', version: kotlinVersion
     }
 }
 
@@ -24,6 +27,8 @@ tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
 }
 
+apply plugin: 'kotlin'
+
 configurations {
     provided {
         dependencies.all { dep ->
@@ -38,18 +43,23 @@ dependencies {
     provided group: 'org.freenetproject', name: 'freenet-ext', version: '29'
     provided group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54'
 
+    compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'
     compile group: 'net.pterodactylus', name: 'utils', version: '0.12.4'
     compile group: 'com.google.inject', name: 'guice', version: '3.0'
     compile group: 'com.google.guava', name: 'guava', version: '14.0.1'
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.1.2'
-    compile group: 'com.google.code.findbugs', name: 'jsr305', version: '2.0.1'
+    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.1'
+    compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.1'
+    compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
+    compile group: 'org.jsoup', name: 'jsoup', version: '1.10.2'
 
+    testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test'
     testCompile group: 'junit', name: 'junit', version: '4.11'
     testCompile group: 'org.mockito', name: 'mockito-core', version: '2.1.0'
-    testCompile group: 'org.jsoup', name: 'jsoup', version: '1.7.1'
     testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
 }
 
+apply from: 'version.gradle'
+
 task fatJar(type: Jar) {
     archiveName = project.name + '-jar-with-dependencies.jar'
     from { (configurations.runtime - configurations.provided).collect { it.isDirectory() ? it : zipTree(it) } }
@@ -72,7 +82,7 @@ javadoc {
 apply plugin: 'jacoco'
 
 jacoco {
-    toolVersion = '0.7.7.201606060606'
+    toolVersion = '0.7.9'
 }
 
 jacocoTestReport.dependsOn test
@@ -92,3 +102,27 @@ findbugs {
 }
 
 apply plugin: 'idea'
+
+task countLinesMain(type: Exec) {
+    executable = 'cloc'
+    args = ['--by-file', '--xml', '--report-file=build/reports/cloc/main.xml', 'src/main']
+    standardOutput = new ByteArrayOutputStream()
+}
+
+task countLinesTest(type: Exec) {
+    executable = 'cloc'
+    args = ['--by-file', '--xml', '--report-file=build/reports/cloc/test.xml', 'src/test']
+    standardOutput = new ByteArrayOutputStream()
+}
+
+task countLines {
+    new File(buildDir, "reports/cloc").mkdirs()
+    dependsOn tasks.countLinesMain
+    dependsOn tasks.countLinesTest
+}
+
+apply plugin: 'kotlin-noarg'
+
+noArg {
+    annotation('net.pterodactylus.sone.main.NoArg')
+}
index 91825a6..66677de 100644 (file)
@@ -40,6 +40,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
@@ -68,8 +71,8 @@ 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.data.Sone.ShowCustomAvatars;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
 import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.sone.database.AlbumBuilder;
 import net.pterodactylus.sone.database.Database;
@@ -314,6 +317,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /**
         * {@inheritDocs}
         */
+       @Nonnull
        @Override
        public Collection<Sone> getSones() {
                return database.getSones();
@@ -534,7 +538,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The album with the given ID, or {@code null} if no album with the
         *         given ID exists
         */
-       public Album getAlbum(String albumId) {
+       @Nullable
+       public Album getAlbum(@Nonnull String albumId) {
                return database.getAlbum(albumId).orNull();
        }
 
@@ -549,6 +554,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            The ID of the image
         * @return The image with the given ID
         */
+       @Nullable
        public Image getImage(String imageId) {
                return getImage(imageId, true);
        }
@@ -565,6 +571,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The image with the given ID, or {@code null} if none exists and
         *         none was created
         */
+       @Nullable
        public Image getImage(String imageId, boolean create) {
                Optional<Image> image = database.getImage(imageId);
                if (image.isPresent()) {
@@ -1070,7 +1077,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(true));
                sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(true));
                sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(true));
-               sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
+               sone.getOptions().setShowCustomAvatars(LoadExternalContent.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(LoadExternalContent.NEVER.name())));
+               sone.getOptions().setLoadLinkedImages(LoadExternalContent.valueOf(configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").getValue(LoadExternalContent.NEVER.name())));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1548,6 +1556,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications());
                        configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications());
                        configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
+                       configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").setValue(sone.getOptions().getLoadLinkedImages().name());
 
                        configuration.save();
 
index a06b5b0..946371b 100644 (file)
@@ -23,6 +23,7 @@ import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
 import static net.pterodactylus.sone.freenet.Key.routingKey;
 
+import java.io.IOException;
 import java.net.MalformedURLException;
 import java.util.Collections;
 import java.util.HashMap;
@@ -30,6 +31,8 @@ import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
@@ -44,6 +47,7 @@ import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import freenet.client.ClientMetadata;
+import freenet.client.FetchContext;
 import freenet.client.FetchException;
 import freenet.client.FetchException.FetchExceptionMode;
 import freenet.client.FetchResult;
@@ -51,16 +55,21 @@ import freenet.client.HighLevelSimpleClient;
 import freenet.client.InsertBlock;
 import freenet.client.InsertContext;
 import freenet.client.InsertException;
+import freenet.client.Metadata;
 import freenet.client.async.BaseClientPutter;
 import freenet.client.async.ClientContext;
+import freenet.client.async.ClientGetCallback;
+import freenet.client.async.ClientGetter;
 import freenet.client.async.ClientPutCallback;
 import freenet.client.async.ClientPutter;
+import freenet.client.async.SnoopMetadata;
 import freenet.client.async.USKCallback;
 import freenet.keys.FreenetURI;
 import freenet.keys.InsertableClientSSK;
 import freenet.keys.USK;
 import freenet.node.Node;
 import freenet.node.RequestClient;
+import freenet.node.RequestClientBuilder;
 import freenet.node.RequestStarter;
 import freenet.support.api.Bucket;
 import freenet.support.api.RandomAccessBucket;
@@ -93,17 +102,8 @@ public class FreenetInterface {
        /** The not-Sone-related USK callbacks. */
        private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
 
-       private final RequestClient imageInserts = new RequestClient() {
-               @Override
-               public boolean persistent() {
-                       return false;
-               }
-
-               @Override
-               public boolean realTimeFlag() {
-                       return true;
-               }
-       };
+       private final RequestClient imageInserts = new RequestClientBuilder().realTime().build();
+       private final RequestClient imageLoader = new RequestClientBuilder().realTime().build();
 
        /**
         * Creates a new Freenet interface.
@@ -148,6 +148,59 @@ public class FreenetInterface {
                }
        }
 
+       public void startFetch(final FreenetURI uri, final BackgroundFetchCallback backgroundFetchCallback) {
+               ClientGetCallback callback = new ClientGetCallback() {
+                       @Override
+                       public void onSuccess(FetchResult result, ClientGetter state) {
+                               try {
+                                       backgroundFetchCallback.loaded(uri, result.getMimeType(), result.asByteArray());
+                               } catch (IOException e) {
+                                       backgroundFetchCallback.failed(uri);
+                               }
+                       }
+
+                       @Override
+                       public void onFailure(FetchException e, ClientGetter state) {
+                               backgroundFetchCallback.failed(uri);
+                       }
+
+                       @Override
+                       public void onResume(ClientContext context) throws ResumeFailedException {
+                               /* do nothing. */
+                       }
+
+                       @Override
+                       public RequestClient getRequestClient() {
+                               return imageLoader;
+                       }
+               };
+               SnoopMetadata snoop = new SnoopMetadata() {
+                       @Override
+                       public boolean snoopMetadata(Metadata meta, ClientContext context) {
+                               String mimeType = meta.getMIMEType();
+                               boolean cancel = (mimeType == null) || backgroundFetchCallback.shouldCancel(uri, mimeType, meta.dataLength());
+                               if (cancel) {
+                                       backgroundFetchCallback.failed(uri);
+                               }
+                               return cancel;
+                       }
+               };
+               FetchContext fetchContext = client.getFetchContext();
+               try {
+                       ClientGetter clientGetter = client.fetch(uri, 2097152, callback, fetchContext, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
+                       clientGetter.setMetaSnoop(snoop);
+                       clientGetter.restart(uri, fetchContext.filterData, node.clientCore.clientContext);
+               } catch (FetchException fe) {
+                       /* stupid exception that can not actually be thrown! */
+               }
+       }
+
+       public interface BackgroundFetchCallback {
+               boolean shouldCancel(@Nonnull FreenetURI uri, @Nonnull String mimeType, long size);
+               void loaded(@Nonnull FreenetURI uri, @Nonnull String mimeType, @Nonnull byte[] data);
+               void failed(@Nonnull FreenetURI uri);
+       }
+
        /**
         * Inserts the image data of the given {@link TemporaryImage} and returns
         * the given insert token that can be used to add listeners or cancel the
index 0f9b8ff..a0ebeba 100644 (file)
@@ -26,6 +26,9 @@ import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 
@@ -80,7 +83,7 @@ public class Profile implements Fingerprintable {
         * @param profile
         *            The profile to copy
         */
-       public Profile(Profile profile) {
+       public Profile(@Nonnull Profile profile) {
                this.sone = profile.sone;
                this.firstName = profile.firstName;
                this.middleName = profile.middleName;
@@ -101,6 +104,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The Sone this profile belongs to
         */
+       @Nonnull
        public Sone getSone() {
                return sone;
        }
@@ -110,6 +114,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The first name
         */
+       @Nullable
        public String getFirstName() {
                return firstName;
        }
@@ -121,7 +126,8 @@ public class Profile implements Fingerprintable {
         *            The first name to set
         * @return This profile (for method chaining)
         */
-       public Profile setFirstName(String firstName) {
+       @Nonnull
+       public Profile setFirstName(@Nullable String firstName) {
                this.firstName = firstName;
                return this;
        }
@@ -131,6 +137,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The middle name
         */
+       @Nullable
        public String getMiddleName() {
                return middleName;
        }
@@ -142,7 +149,8 @@ public class Profile implements Fingerprintable {
         *            The middle name to set
         * @return This profile (for method chaining)
         */
-       public Profile setMiddleName(String middleName) {
+       @Nonnull
+       public Profile setMiddleName(@Nullable String middleName) {
                this.middleName = middleName;
                return this;
        }
@@ -152,6 +160,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The last name
         */
+       @Nullable
        public String getLastName() {
                return lastName;
        }
@@ -163,7 +172,8 @@ public class Profile implements Fingerprintable {
         *            The last name to set
         * @return This profile (for method chaining)
         */
-       public Profile setLastName(String lastName) {
+       @Nonnull
+       public Profile setLastName(@Nullable String lastName) {
                this.lastName = lastName;
                return this;
        }
@@ -173,6 +183,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The day of the birth date (from 1 to 31)
         */
+       @Nullable
        public Integer getBirthDay() {
                return birthDay;
        }
@@ -184,7 +195,8 @@ public class Profile implements Fingerprintable {
         *            The day of the birth date (from 1 to 31)
         * @return This profile (for method chaining)
         */
-       public Profile setBirthDay(Integer birthDay) {
+       @Nonnull
+       public Profile setBirthDay(@Nullable Integer birthDay) {
                this.birthDay = birthDay;
                return this;
        }
@@ -194,6 +206,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The month of the birth date (from 1 to 12)
         */
+       @Nullable
        public Integer getBirthMonth() {
                return birthMonth;
        }
@@ -205,7 +218,8 @@ public class Profile implements Fingerprintable {
         *            The month of the birth date (from 1 to 12)
         * @return This profile (for method chaining)
         */
-       public Profile setBirthMonth(Integer birthMonth) {
+       @Nonnull
+       public Profile setBirthMonth(@Nullable Integer birthMonth) {
                this.birthMonth = birthMonth;
                return this;
        }
@@ -215,6 +229,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The year of the birth date
         */
+       @Nullable
        public Integer getBirthYear() {
                return birthYear;
        }
@@ -225,6 +240,7 @@ public class Profile implements Fingerprintable {
         * @return The ID of the currently selected avatar image, or {@code null} if
         *         no avatar is selected.
         */
+       @Nullable
        public String getAvatar() {
                return avatar;
        }
@@ -237,7 +253,8 @@ public class Profile implements Fingerprintable {
         *            image.
         * @return This Sone
         */
-       public Profile setAvatar(Image avatar) {
+       @Nonnull
+       public Profile setAvatar(@Nullable Image avatar) {
                if (avatar == null) {
                        this.avatar = null;
                        return this;
@@ -254,7 +271,8 @@ public class Profile implements Fingerprintable {
         *            The year of the birth date
         * @return This profile (for method chaining)
         */
-       public Profile setBirthYear(Integer birthYear) {
+       @Nonnull
+       public Profile setBirthYear(@Nullable Integer birthYear) {
                this.birthYear = birthYear;
                return this;
        }
@@ -264,6 +282,7 @@ public class Profile implements Fingerprintable {
         *
         * @return The fields of this profile
         */
+       @Nonnull
        public List<Field> getFields() {
                return new ArrayList<Field>(fields);
        }
@@ -275,7 +294,7 @@ public class Profile implements Fingerprintable {
         *            The field to check for
         * @return {@code true} if this profile contains the field, false otherwise
         */
-       public boolean hasField(Field field) {
+       public boolean hasField(@Nonnull Field field) {
                return fields.contains(field);
        }
 
@@ -287,7 +306,8 @@ public class Profile implements Fingerprintable {
         * @return The field, or {@code null} if this profile does not contain a
         *         field with the given ID
         */
-       public Field getFieldById(String fieldId) {
+       @Nullable
+       public Field getFieldById(@Nonnull String fieldId) {
                checkNotNull(fieldId, "fieldId must not be null");
                for (Field field : fields) {
                        if (field.getId().equals(fieldId)) {
@@ -305,7 +325,8 @@ public class Profile implements Fingerprintable {
         * @return The field, or {@code null} if this profile does not contain a
         *         field with the given name
         */
-       public Field getFieldByName(String fieldName) {
+       @Nullable
+       public Field getFieldByName(@Nonnull String fieldName) {
                for (Field field : fields) {
                        if (field.getName().equals(fieldName)) {
                                return field;
@@ -323,7 +344,8 @@ public class Profile implements Fingerprintable {
         * @throws IllegalArgumentException
         *             if the name is not valid
         */
-       public Field addField(String fieldName) throws IllegalArgumentException {
+       @Nonnull
+       public Field addField(@Nonnull String fieldName) throws IllegalArgumentException {
                checkNotNull(fieldName, "fieldName must not be null");
                if (fieldName.length() == 0) {
                        throw new EmptyFieldName();
@@ -345,7 +367,7 @@ public class Profile implements Fingerprintable {
         * @param field
         *            The field to move up
         */
-       public void moveFieldUp(Field field) {
+       public void moveFieldUp(@Nonnull Field field) {
                checkNotNull(field, "field must not be null");
                checkArgument(hasField(field), "field must belong to this profile");
                checkArgument(getFieldIndex(field) > 0, "field index must be > 0");
@@ -362,7 +384,7 @@ public class Profile implements Fingerprintable {
         * @param field
         *            The field to move down
         */
-       public void moveFieldDown(Field field) {
+       public void moveFieldDown(@Nonnull Field field) {
                checkNotNull(field, "field must not be null");
                checkArgument(hasField(field), "field must belong to this profile");
                checkArgument(getFieldIndex(field) < fields.size() - 1, "field index must be < " + (fields.size() - 1));
@@ -377,7 +399,7 @@ public class Profile implements Fingerprintable {
         * @param field
         *            The field to remove
         */
-       public void removeField(Field field) {
+       public void removeField(@Nonnull Field field) {
                checkNotNull(field, "field must not be null");
                checkArgument(hasField(field), "field must belong to this profile");
                fields.remove(field);
@@ -395,7 +417,7 @@ public class Profile implements Fingerprintable {
         * @return The index of the field, or {@code -1} if there is no field with
         *         the given name
         */
-       private int getFieldIndex(Field field) {
+       private int getFieldIndex(@Nonnull Field field) {
                return fields.indexOf(field);
        }
 
@@ -470,7 +492,7 @@ public class Profile implements Fingerprintable {
                 * @param id
                 *            The ID of the field
                 */
-               private Field(String id) {
+               private Field(@Nonnull String id) {
                        this.id = checkNotNull(id, "id must not be null");
                }
 
@@ -479,6 +501,7 @@ public class Profile implements Fingerprintable {
                 *
                 * @return The ID of this field
                 */
+               @Nonnull
                public String getId() {
                        return id;
                }
@@ -488,6 +511,7 @@ public class Profile implements Fingerprintable {
                 *
                 * @return The name of this field
                 */
+               @Nonnull
                public String getName() {
                        return name;
                }
@@ -501,7 +525,8 @@ public class Profile implements Fingerprintable {
                 *            The new name of this field
                 * @return This field
                 */
-               public Field setName(String name) {
+               @Nonnull
+               public Field setName(@Nonnull String name) {
                        checkNotNull(name, "name must not be null");
                        checkArgument(getFieldByName(name) == null, "name must be unique");
                        this.name = name;
@@ -513,6 +538,7 @@ public class Profile implements Fingerprintable {
                 *
                 * @return The value of this field
                 */
+               @Nullable
                public String getValue() {
                        return value;
                }
@@ -526,7 +552,8 @@ public class Profile implements Fingerprintable {
                 *            The new value of this field
                 * @return This field
                 */
-               public Field setValue(String value) {
+               @Nonnull
+               public Field setValue(@Nullable String value) {
                        this.value = value;
                        return this;
                }
index 68c8114..b761c7b 100644 (file)
@@ -69,30 +69,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
                downloading,
        }
 
-       /**
-        * The possible values for the “show custom avatars” option.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static enum ShowCustomAvatars {
-
-               /** Never show custom avatars. */
-               NEVER,
-
-               /** Only show custom avatars of followed Sones. */
-               FOLLOWED,
-
-               /** Only show custom avatars of Sones you manually trust. */
-               MANUALLY_TRUSTED,
-
-               /** Only show custom avatars of automatically trusted Sones. */
-               TRUSTED,
-
-               /** Always show custom avatars. */
-               ALWAYS,
-
-       }
-
        /** comparator that sorts Sones by their nice name. */
        public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
 
@@ -193,6 +169,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The identity of this Sone
         */
+       @Nonnull
        Identity getIdentity();
 
        /**
@@ -200,6 +177,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The name of this Sone
         */
+       @Nonnull
        String getName();
 
        /**
@@ -214,6 +192,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The request URI of this Sone
         */
+       @Nonnull
        FreenetURI getRequestUri();
 
        /**
@@ -221,6 +200,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The insert URI of this Sone
         */
+       @Nullable
        FreenetURI getInsertUri();
 
        /**
@@ -254,6 +234,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The time of the update (in milliseconds since Jan 1, 1970 UTC)
         * @return This Sone (for method chaining)
         */
+       @Nonnull
        Sone setTime(long time);
 
        /**
@@ -261,6 +242,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The status of this Sone
         */
+       @Nonnull
        SoneStatus getStatus();
 
        /**
@@ -272,7 +254,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @throws IllegalArgumentException
         *              if {@code status} is {@code null}
         */
-       Sone setStatus(SoneStatus status);
+       @Nonnull
+       Sone setStatus(@Nonnull SoneStatus status);
 
        /**
         * Returns a copy of the profile. If you want to update values in the profile
@@ -281,6 +264,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return A copy of the profile
         */
+       @Nonnull
        Profile getProfile();
 
        /**
@@ -291,13 +275,14 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @param profile
         *              The profile to set
         */
-       void setProfile(Profile profile);
+       void setProfile(@Nonnull Profile profile);
 
        /**
         * Returns the client used by this Sone.
         *
         * @return The client used by this Sone, or {@code null}
         */
+       @Nullable
        Client getClient();
 
        /**
@@ -307,7 +292,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The client used by this Sone, or {@code null}
         * @return This Sone (for method chaining)
         */
-       Sone setClient(Client client);
+       @Nonnull
+       Sone setClient(@Nullable Client client);
 
        /**
         * Returns whether this Sone is known.
@@ -323,6 +309,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              {@code true} if this Sone is known, {@code false} otherwise
         * @return This Sone
         */
+       @Nonnull
        Sone setKnown(boolean known);
 
        /**
@@ -330,6 +317,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The friend Sones of this Sone
         */
+       @Nonnull
        Collection<String> getFriends();
 
        /**
@@ -340,13 +328,14 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @return {@code true} if this Sone has the given Sone as a friend, {@code
         *         false} otherwise
         */
-       boolean hasFriend(String friendSoneId);
+       boolean hasFriend(@Nonnull String friendSoneId);
 
        /**
         * Returns the list of posts of this Sone, sorted by time, newest first.
         *
         * @return All posts of this Sone
         */
+       @Nonnull
        List<Post> getPosts();
 
        /**
@@ -356,7 +345,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The new (and only) posts of this Sone
         * @return This Sone (for method chaining)
         */
-       Sone setPosts(Collection<Post> posts);
+       @Nonnull
+       Sone setPosts(@Nonnull Collection<Post> posts);
 
        /**
         * Adds the given post to this Sone. The post will not be added if its {@link
@@ -365,7 +355,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @param post
         *              The post to add
         */
-       void addPost(Post post);
+       void addPost(@Nonnull Post post);
 
        /**
         * Removes the given post from this Sone.
@@ -373,13 +363,14 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @param post
         *              The post to remove
         */
-       void removePost(Post post);
+       void removePost(@Nonnull Post post);
 
        /**
         * Returns all replies this Sone made.
         *
         * @return All replies this Sone made
         */
+       @Nonnull
        Set<PostReply> getReplies();
 
        /**
@@ -389,7 +380,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The new (and only) replies of this Sone
         * @return This Sone (for method chaining)
         */
-       Sone setReplies(Collection<PostReply> replies);
+       @Nonnull
+       Sone setReplies(@Nonnull Collection<PostReply> replies);
 
        /**
         * Adds a reply to this Sone. If the given reply was not made by this Sone,
@@ -398,7 +390,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @param reply
         *              The reply to add
         */
-       void addReply(PostReply reply);
+       void addReply(@Nonnull PostReply reply);
 
        /**
         * Removes a reply from this Sone.
@@ -406,13 +398,14 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @param reply
         *              The reply to remove
         */
-       void removeReply(PostReply reply);
+       void removeReply(@Nonnull PostReply reply);
 
        /**
         * Returns the IDs of all liked posts.
         *
         * @return All liked posts’ IDs
         */
+       @Nonnull
        Set<String> getLikedPostIds();
 
        /**
@@ -422,7 +415,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              All liked posts’ IDs
         * @return This Sone (for method chaining)
         */
-       Sone setLikePostIds(Set<String> likedPostIds);
+       @Nonnull
+       Sone setLikePostIds(@Nonnull Set<String> likedPostIds);
 
        /**
         * Checks whether the given post ID is liked by this Sone.
@@ -432,7 +426,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @return {@code true} if this Sone likes the given post, {@code false}
         *         otherwise
         */
-       boolean isLikedPostId(String postId);
+       boolean isLikedPostId(@Nonnull String postId);
 
        /**
         * Adds the given post ID to the list of posts this Sone likes.
@@ -441,22 +435,23 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The ID of the post
         * @return This Sone (for method chaining)
         */
-       Sone addLikedPostId(String postId);
+       @Nonnull
+       Sone addLikedPostId(@Nonnull String postId);
 
        /**
         * Removes the given post ID from the list of posts this Sone likes.
         *
         * @param postId
         *              The ID of the post
-        * @return This Sone (for method chaining)
         */
-       Sone removeLikedPostId(String postId);
+       void removeLikedPostId(@Nonnull String postId);
 
        /**
         * Returns the IDs of all liked replies.
         *
         * @return All liked replies’ IDs
         */
+       @Nonnull
        Set<String> getLikedReplyIds();
 
        /**
@@ -466,7 +461,8 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              All liked replies’ IDs
         * @return This Sone (for method chaining)
         */
-       Sone setLikeReplyIds(Set<String> likedReplyIds);
+       @Nonnull
+       Sone setLikeReplyIds(@Nonnull Set<String> likedReplyIds);
 
        /**
         * Checks whether the given reply ID is liked by this Sone.
@@ -476,7 +472,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         * @return {@code true} if this Sone likes the given reply, {@code false}
         *         otherwise
         */
-       boolean isLikedReplyId(String replyId);
+       boolean isLikedReplyId(@Nonnull String replyId);
 
        /**
         * Adds the given reply ID to the list of replies this Sone likes.
@@ -485,22 +481,23 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The ID of the reply
         * @return This Sone (for method chaining)
         */
-       Sone addLikedReplyId(String replyId);
+       @Nonnull
+       Sone addLikedReplyId(@Nonnull String replyId);
 
        /**
         * Removes the given post ID from the list of replies this Sone likes.
         *
         * @param replyId
         *              The ID of the reply
-        * @return This Sone (for method chaining)
         */
-       Sone removeLikedReplyId(String replyId);
+       void removeLikedReplyId(@Nonnull String replyId);
 
        /**
         * Returns the root album that contains all visible albums of this Sone.
         *
         * @return The root album of this Sone
         */
+       @Nonnull
        Album getRootAlbum();
 
        /**
@@ -508,6 +505,7 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *
         * @return The options of this Sone
         */
+       @Nonnull
        SoneOptions getOptions();
 
        /**
@@ -517,6 +515,6 @@ public interface Sone extends Identified, Fingerprintable, Comparable<Sone> {
         *              The options of this Sone
         */
        /* TODO - remove this method again, maybe add an option provider */
-       void setOptions(SoneOptions options);
+       void setOptions(@Nonnull SoneOptions options);
 
 }
index 819e94a..5f05447 100644 (file)
@@ -1,8 +1,8 @@
 package net.pterodactylus.sone.data;
 
-import static net.pterodactylus.sone.data.Sone.ShowCustomAvatars.NEVER;
+import static net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.NEVER;
 
-import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
+import javax.annotation.Nonnull;
 
 /**
  * All Sone-specific options.
@@ -26,8 +26,35 @@ public interface SoneOptions {
        boolean isShowNewReplyNotifications();
        void setShowNewReplyNotifications(boolean showNewReplyNotifications);
 
-       ShowCustomAvatars getShowCustomAvatars();
-       void setShowCustomAvatars(ShowCustomAvatars showCustomAvatars);
+       LoadExternalContent getShowCustomAvatars();
+       void setShowCustomAvatars(LoadExternalContent showCustomAvatars);
+
+       @Nonnull LoadExternalContent getLoadLinkedImages();
+       void setLoadLinkedImages(@Nonnull LoadExternalContent loadLinkedImages);
+
+       /**
+        * Possible values for all options that are related to loading external content.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       enum LoadExternalContent {
+
+               /** Never show custom avatars. */
+               NEVER,
+
+               /** Only show custom avatars of followed Sones. */
+               FOLLOWED,
+
+               /** Only show custom avatars of Sones you manually trust. */
+               MANUALLY_TRUSTED,
+
+               /** Only show custom avatars of automatically trusted Sones. */
+               TRUSTED,
+
+               /** Always show custom avatars. */
+               ALWAYS,
+
+       }
 
        /**
         * {@link SoneOptions} implementation.
@@ -41,7 +68,8 @@ public interface SoneOptions {
                private boolean showNewSoneNotifications = true;
                private boolean showNewPostNotifications = true;
                private boolean showNewReplyNotifications = true;
-               private ShowCustomAvatars showCustomAvatars = NEVER;
+               private LoadExternalContent showCustomAvatars = NEVER;
+               private LoadExternalContent loadLinkedImages = NEVER;
 
                @Override
                public boolean isAutoFollow() {
@@ -94,15 +122,26 @@ public interface SoneOptions {
                }
 
                @Override
-               public ShowCustomAvatars getShowCustomAvatars() {
+               public LoadExternalContent getShowCustomAvatars() {
                        return showCustomAvatars;
                }
 
                @Override
-               public void setShowCustomAvatars(ShowCustomAvatars showCustomAvatars) {
+               public void setShowCustomAvatars(LoadExternalContent showCustomAvatars) {
                        this.showCustomAvatars = showCustomAvatars;
                }
 
+               @Nonnull
+               @Override
+               public LoadExternalContent getLoadLinkedImages() {
+                       return loadLinkedImages;
+               }
+
+               @Override
+               public void setLoadLinkedImages(@Nonnull LoadExternalContent loadLinkedImages) {
+                       this.loadLinkedImages = loadLinkedImages;
+               }
+
        }
 
 }
index e9a0c57..79766b5 100644 (file)
@@ -184,8 +184,7 @@ public class IdOnlySone implements Sone {
        }
 
        @Override
-       public Sone removeLikedPostId(String postId) {
-               return this;
+       public void removeLikedPostId(String postId) {
        }
 
        @Override
@@ -209,8 +208,7 @@ public class IdOnlySone implements Sone {
        }
 
        @Override
-       public Sone removeLikedReplyId(String replyId) {
-               return this;
+       public void removeLikedReplyId(String replyId) {
        }
 
        @Override
index 04a5bcc..0494607 100644 (file)
@@ -31,6 +31,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
@@ -134,6 +137,7 @@ public class SoneImpl implements Sone {
         *
         * @return The identity of this Sone
         */
+       @Nonnull
        public String getId() {
                return id;
        }
@@ -143,6 +147,7 @@ public class SoneImpl implements Sone {
         *
         * @return The identity of this Sone
         */
+       @Nonnull
        public Identity getIdentity() {
                return identity;
        }
@@ -152,6 +157,7 @@ public class SoneImpl implements Sone {
         *
         * @return The name of this Sone
         */
+       @Nonnull
        public String getName() {
                return (identity != null) ? identity.getNickname() : null;
        }
@@ -170,6 +176,7 @@ public class SoneImpl implements Sone {
         *
         * @return The request URI of this Sone
         */
+       @Nonnull
        public FreenetURI getRequestUri() {
                try {
                        return new FreenetURI(getIdentity().getRequestUri())
@@ -189,6 +196,7 @@ public class SoneImpl implements Sone {
         *
         * @return The insert URI of this Sone
         */
+       @Nullable
        public FreenetURI getInsertUri() {
                if (!isLocal()) {
                        return null;
@@ -244,6 +252,7 @@ public class SoneImpl implements Sone {
         *              The time of the update (in milliseconds since Jan 1, 1970 UTC)
         * @return This Sone (for method chaining)
         */
+       @Nonnull
        public Sone setTime(long time) {
                this.time = time;
                return this;
@@ -254,6 +263,7 @@ public class SoneImpl implements Sone {
         *
         * @return The status of this Sone
         */
+       @Nonnull
        public SoneStatus getStatus() {
                return status;
        }
@@ -267,7 +277,8 @@ public class SoneImpl implements Sone {
         * @throws IllegalArgumentException
         *              if {@code status} is {@code null}
         */
-       public Sone setStatus(SoneStatus status) {
+       @Nonnull
+       public Sone setStatus(@Nonnull SoneStatus status) {
                this.status = checkNotNull(status, "status must not be null");
                return this;
        }
@@ -279,6 +290,7 @@ public class SoneImpl implements Sone {
         *
         * @return A copy of the profile
         */
+       @Nonnull
        public Profile getProfile() {
                return new Profile(profile);
        }
@@ -291,7 +303,7 @@ public class SoneImpl implements Sone {
         * @param profile
         *              The profile to set
         */
-       public void setProfile(Profile profile) {
+       public void setProfile(@Nonnull Profile profile) {
                this.profile = new Profile(profile);
        }
 
@@ -300,6 +312,7 @@ public class SoneImpl implements Sone {
         *
         * @return The client used by this Sone, or {@code null}
         */
+       @Nullable
        public Client getClient() {
                return client;
        }
@@ -311,7 +324,8 @@ public class SoneImpl implements Sone {
         *              The client used by this Sone, or {@code null}
         * @return This Sone (for method chaining)
         */
-       public Sone setClient(Client client) {
+       @Nonnull
+       public Sone setClient(@Nullable Client client) {
                this.client = client;
                return this;
        }
@@ -332,6 +346,7 @@ public class SoneImpl implements Sone {
         *              {@code true} if this Sone is known, {@code false} otherwise
         * @return This Sone
         */
+       @Nonnull
        public Sone setKnown(boolean known) {
                this.known = known;
                return this;
@@ -342,6 +357,7 @@ public class SoneImpl implements Sone {
         *
         * @return The friend Sones of this Sone
         */
+       @Nonnull
        public Collection<String> getFriends() {
                return database.getFriends(this);
        }
@@ -354,7 +370,7 @@ public class SoneImpl implements Sone {
         * @return {@code true} if this Sone has the given Sone as a friend, {@code
         *         false} otherwise
         */
-       public boolean hasFriend(String friendSoneId) {
+       public boolean hasFriend(@Nonnull String friendSoneId) {
                return database.isFriend(this, friendSoneId);
        }
 
@@ -363,6 +379,7 @@ public class SoneImpl implements Sone {
         *
         * @return All posts of this Sone
         */
+       @Nonnull
        public List<Post> getPosts() {
                List<Post> sortedPosts;
                synchronized (this) {
@@ -379,7 +396,8 @@ public class SoneImpl implements Sone {
         *              The new (and only) posts of this Sone
         * @return This Sone (for method chaining)
         */
-       public Sone setPosts(Collection<Post> posts) {
+       @Nonnull
+       public Sone setPosts(@Nonnull Collection<Post> posts) {
                synchronized (this) {
                        this.posts.clear();
                        this.posts.addAll(posts);
@@ -394,7 +412,7 @@ public class SoneImpl implements Sone {
         * @param post
         *              The post to add
         */
-       public void addPost(Post post) {
+       public void addPost(@Nonnull Post post) {
                if (post.getSone().equals(this) && posts.add(post)) {
                        logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
                }
@@ -406,7 +424,7 @@ public class SoneImpl implements Sone {
         * @param post
         *              The post to remove
         */
-       public void removePost(Post post) {
+       public void removePost(@Nonnull Post post) {
                if (post.getSone().equals(this)) {
                        posts.remove(post);
                }
@@ -417,6 +435,7 @@ public class SoneImpl implements Sone {
         *
         * @return All replies this Sone made
         */
+       @Nonnull
        public Set<PostReply> getReplies() {
                return Collections.unmodifiableSet(replies);
        }
@@ -428,7 +447,8 @@ public class SoneImpl implements Sone {
         *              The new (and only) replies of this Sone
         * @return This Sone (for method chaining)
         */
-       public Sone setReplies(Collection<PostReply> replies) {
+       @Nonnull
+       public Sone setReplies(@Nonnull Collection<PostReply> replies) {
                this.replies.clear();
                this.replies.addAll(replies);
                return this;
@@ -441,7 +461,7 @@ public class SoneImpl implements Sone {
         * @param reply
         *              The reply to add
         */
-       public void addReply(PostReply reply) {
+       public void addReply(@Nonnull PostReply reply) {
                if (reply.getSone().equals(this)) {
                        replies.add(reply);
                }
@@ -453,7 +473,7 @@ public class SoneImpl implements Sone {
         * @param reply
         *              The reply to remove
         */
-       public void removeReply(PostReply reply) {
+       public void removeReply(@Nonnull PostReply reply) {
                if (reply.getSone().equals(this)) {
                        replies.remove(reply);
                }
@@ -464,6 +484,7 @@ public class SoneImpl implements Sone {
         *
         * @return All liked posts’ IDs
         */
+       @Nonnull
        public Set<String> getLikedPostIds() {
                return Collections.unmodifiableSet(likedPostIds);
        }
@@ -475,7 +496,8 @@ public class SoneImpl implements Sone {
         *              All liked posts’ IDs
         * @return This Sone (for method chaining)
         */
-       public Sone setLikePostIds(Set<String> likedPostIds) {
+       @Nonnull
+       public Sone setLikePostIds(@Nonnull Set<String> likedPostIds) {
                this.likedPostIds.clear();
                this.likedPostIds.addAll(likedPostIds);
                return this;
@@ -489,7 +511,7 @@ public class SoneImpl implements Sone {
         * @return {@code true} if this Sone likes the given post, {@code false}
         *         otherwise
         */
-       public boolean isLikedPostId(String postId) {
+       public boolean isLikedPostId(@Nonnull String postId) {
                return likedPostIds.contains(postId);
        }
 
@@ -500,7 +522,8 @@ public class SoneImpl implements Sone {
         *              The ID of the post
         * @return This Sone (for method chaining)
         */
-       public Sone addLikedPostId(String postId) {
+       @Nonnull
+       public Sone addLikedPostId(@Nonnull String postId) {
                likedPostIds.add(postId);
                return this;
        }
@@ -510,11 +533,9 @@ public class SoneImpl implements Sone {
         *
         * @param postId
         *              The ID of the post
-        * @return This Sone (for method chaining)
         */
-       public Sone removeLikedPostId(String postId) {
+       public void removeLikedPostId(@Nonnull String postId) {
                likedPostIds.remove(postId);
-               return this;
        }
 
        /**
@@ -522,6 +543,7 @@ public class SoneImpl implements Sone {
         *
         * @return All liked replies’ IDs
         */
+       @Nonnull
        public Set<String> getLikedReplyIds() {
                return Collections.unmodifiableSet(likedReplyIds);
        }
@@ -533,7 +555,8 @@ public class SoneImpl implements Sone {
         *              All liked replies’ IDs
         * @return This Sone (for method chaining)
         */
-       public Sone setLikeReplyIds(Set<String> likedReplyIds) {
+       @Nonnull
+       public Sone setLikeReplyIds(@Nonnull Set<String> likedReplyIds) {
                this.likedReplyIds.clear();
                this.likedReplyIds.addAll(likedReplyIds);
                return this;
@@ -547,7 +570,7 @@ public class SoneImpl implements Sone {
         * @return {@code true} if this Sone likes the given reply, {@code false}
         *         otherwise
         */
-       public boolean isLikedReplyId(String replyId) {
+       public boolean isLikedReplyId(@Nonnull String replyId) {
                return likedReplyIds.contains(replyId);
        }
 
@@ -558,7 +581,8 @@ public class SoneImpl implements Sone {
         *              The ID of the reply
         * @return This Sone (for method chaining)
         */
-       public Sone addLikedReplyId(String replyId) {
+       @Nonnull
+       public Sone addLikedReplyId(@Nonnull String replyId) {
                likedReplyIds.add(replyId);
                return this;
        }
@@ -568,11 +592,9 @@ public class SoneImpl implements Sone {
         *
         * @param replyId
         *              The ID of the reply
-        * @return This Sone (for method chaining)
         */
-       public Sone removeLikedReplyId(String replyId) {
+       public void removeLikedReplyId(@Nonnull String replyId) {
                likedReplyIds.remove(replyId);
-               return this;
        }
 
        /**
@@ -580,6 +602,7 @@ public class SoneImpl implements Sone {
         *
         * @return The root album of this Sone
         */
+       @Nonnull
        public Album getRootAlbum() {
                return rootAlbum;
        }
@@ -589,6 +612,7 @@ public class SoneImpl implements Sone {
         *
         * @return The options of this Sone
         */
+       @Nonnull
        public SoneOptions getOptions() {
                return options;
        }
@@ -600,7 +624,7 @@ public class SoneImpl implements Sone {
         *              The options of this Sone
         */
        /* TODO - remove this method again, maybe add an option provider */
-       public void setOptions(SoneOptions options) {
+       public void setOptions(@Nonnull SoneOptions options) {
                this.options = options;
        }
 
index a39ceff..69f7eaf 100644 (file)
@@ -19,6 +19,8 @@ package net.pterodactylus.sone.database;
 
 import java.util.Collection;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 
@@ -51,6 +53,7 @@ public interface SoneProvider {
         *
         * @return All Sones
         */
+       @Nonnull
        public Collection<Sone> getSones();
 
        /**
index b2264a5..98a43ab 100644 (file)
@@ -25,7 +25,6 @@ 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.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.AbstractCommand;
@@ -33,12 +32,11 @@ import net.pterodactylus.sone.freenet.fcp.Command;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import net.pterodactylus.sone.template.SoneAccessor;
 
-import com.google.common.base.Optional;
-import com.google.common.collect.Collections2;
-
 import freenet.node.FSParseException;
 import freenet.support.SimpleFieldSet;
 
+import com.google.common.base.Optional;
+
 /**
  * Abstract base implementation of a {@link Command} with Sone-related helper
  * methods.
@@ -114,7 +112,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
         * @return The encoded text
         */
        protected static String encodeString(String text) {
-               return text.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
+               return text.replaceAll("\\\\", "\\\\\\\\").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
        }
 
        /**
@@ -164,7 +162,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
                        throw new FcpException("Could not load Sone ID from “" + parameterName + "”.");
                }
                Optional<Sone> sone = core.getSone(soneId);
-               if ((mandatory && !sone.isPresent()) || (mandatory && sone.isPresent() && (localOnly && !sone.get().isLocal()))) {
+               if ((mandatory && !sone.isPresent()) || (sone.isPresent() && localOnly && !sone.get().isLocal())) {
                        throw new FcpException("Could not load Sone from “" + soneId + "”.");
                }
                return sone;
@@ -239,6 +237,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
        protected static SimpleFieldSet encodeSone(Sone sone, String prefix, Optional<Sone> localSone) {
                SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
 
+               soneBuilder.put(prefix + "ID", sone.getId());
                soneBuilder.put(prefix + "Name", sone.getName());
                soneBuilder.put(prefix + "NiceName", SoneAccessor.getNiceName(sone));
                soneBuilder.put(prefix + "LastUpdated", sone.getTime());
@@ -274,10 +273,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
                soneBuilder.put(prefix + "Count", sones.size());
                for (Sone sone : sones) {
                        String sonePrefix = prefix + soneIndex++ + ".";
-                       soneBuilder.put(sonePrefix + "ID", sone.getId());
-                       soneBuilder.put(sonePrefix + "Name", sone.getName());
-                       soneBuilder.put(sonePrefix + "NiceName", SoneAccessor.getNiceName(sone));
-                       soneBuilder.put(sonePrefix + "Time", sone.getTime());
+                       soneBuilder.put(encodeSone(sone, sonePrefix, Optional.<Sone>absent()));
                }
 
                return soneBuilder.get();
@@ -337,9 +333,6 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
                for (Post post : posts) {
                        String postPrefix = prefix + postIndex++;
                        postBuilder.put(encodePost(post, postPrefix + ".", includeReplies));
-                       if (includeReplies) {
-                               postBuilder.put(encodeReplies(Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER), postPrefix + "."));
-                       }
                }
 
                return postBuilder.get();
@@ -355,7 +348,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
         *            {@code null})
         * @return The simple field set containing the replies
         */
-       protected static SimpleFieldSet encodeReplies(Collection<? extends PostReply> replies, String prefix) {
+       protected SimpleFieldSet encodeReplies(Collection<? extends PostReply> replies, String prefix) {
                SimpleFieldSetBuilder replyBuilder = new SimpleFieldSetBuilder();
 
                int replyIndex = 0;
@@ -366,6 +359,7 @@ public abstract class AbstractSoneCommand extends AbstractCommand {
                        replyBuilder.put(replyPrefix + "Sone", reply.getSone().getId());
                        replyBuilder.put(replyPrefix + "Time", reply.getTime());
                        replyBuilder.put(replyPrefix + "Text", encodeString(reply.getText()));
+                       replyBuilder.put(encodeLikes(core.getLikes(reply), replyPrefix + "Likes."));
                }
 
                return replyBuilder.get();
index 6f1e82c..a3f709e 100644 (file)
@@ -25,12 +25,11 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * FCP command that creates a new {@link Post}.
  *
- * @see Core#createPost(Sone, Sone, String)
+ * @see Core#createPost(Sone, Optional, String)
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 public class CreatePostCommand extends AbstractSoneCommand {
@@ -49,7 +48,7 @@ public class CreatePostCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Sone sone = getSone(parameters, "Sone", true);
                String text = getString(parameters, "Text");
                Sone recipient = null;
index 9369529..d268a07 100644 (file)
@@ -25,7 +25,6 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * FCP command that creates a new {@link Reply}.
@@ -49,7 +48,7 @@ public class CreateReplyCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Sone sone = getSone(parameters, "Sone", true);
                Post post = getPost(parameters, "Post");
                String text = getString(parameters, "Text");
index e68cbb5..b5ace1d 100644 (file)
@@ -22,7 +22,6 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * FCP command that deletes a {@link Post}.
@@ -46,11 +45,12 @@ public class DeletePostCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Post post = getPost(parameters, "Post");
                if (!post.getSone().isLocal()) {
                        return new ErrorResponse(401, "Not allowed.");
                }
+               getCore().deletePost(post);
                return new Response("PostDeleted", new SimpleFieldSetBuilder().get());
        }
 
index 00a5b97..d75603e 100644 (file)
@@ -22,7 +22,6 @@ import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * FCP command that deletes a {@link PostReply}.
@@ -46,11 +45,12 @@ public class DeleteReplyCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                PostReply reply = getReply(parameters, "Reply");
                if (!reply.getSone().isLocal()) {
                        return new ErrorResponse(401, "Not allowed.");
                }
+               getCore().deleteReply(reply);
                return new Response("ReplyDeleted", new SimpleFieldSetBuilder().get());
        }
 
index 440cb4c..6f28518 100644 (file)
@@ -19,8 +19,10 @@ package net.pterodactylus.sone.fcp;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.logging.Logger.getLogger;
+import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO;
+import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING;
+import static net.pterodactylus.sone.freenet.fcp.Command.AccessType.RESTRICTED_FCP;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -28,6 +30,9 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.annotation.Nonnull;
+import javax.inject.Singleton;
+
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent;
 import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent;
@@ -45,7 +50,6 @@ import freenet.support.api.Bucket;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 
 /**
  * Implementation of an FCP interface for other clients or plugins to
@@ -84,7 +88,8 @@ public class FcpInterface {
        private final AtomicReference<FullAccessRequired> fullAccessRequired = new AtomicReference<FullAccessRequired>(FullAccessRequired.ALWAYS);
 
        /** All available FCP commands. */
-       private final Map<String, AbstractSoneCommand> commands = Collections.synchronizedMap(new HashMap<String, AbstractSoneCommand>());
+       private final Map<String, AbstractSoneCommand> commands;
+       private final AccessAuthorizer accessAuthorizer;
 
        /**
         * Creates a new FCP interface.
@@ -93,22 +98,9 @@ public class FcpInterface {
         *            The core
         */
        @Inject
-       public FcpInterface(Core core) {
-               commands.put("Version", new VersionCommand(core));
-               commands.put("GetLocalSones", new GetLocalSonesCommand(core));
-               commands.put("GetSones", new GetSonesCommand(core));
-               commands.put("GetSone", new GetSoneCommand(core));
-               commands.put("GetPost", new GetPostCommand(core));
-               commands.put("GetPosts", new GetPostsCommand(core));
-               commands.put("GetPostFeed", new GetPostFeedCommand(core));
-               commands.put("LockSone", new LockSoneCommand(core));
-               commands.put("UnlockSone", new UnlockSoneCommand(core));
-               commands.put("LikePost", new LikePostCommand(core));
-               commands.put("LikeReply", new LikeReplyCommand(core));
-               commands.put("CreatePost", new CreatePostCommand(core));
-               commands.put("CreateReply", new CreateReplyCommand(core));
-               commands.put("DeletePost", new DeletePostCommand(core));
-               commands.put("DeleteReply", new DeleteReplyCommand(core));
+       public FcpInterface(Core core, CommandSupplier commandSupplier, AccessAuthorizer accessAuthorizer) {
+               commands = commandSupplier.supplyCommands(core);
+               this.accessAuthorizer = accessAuthorizer;
        }
 
        //
@@ -152,42 +144,38 @@ public class FcpInterface {
         *            {@link FredPluginFCP#ACCESS_FCP_RESTRICTED}
         */
        public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+               String identifier = parameters.get("Identifier");
+               if ((identifier == null) || (identifier.length() == 0)) {
+                       sendErrorReply(pluginReplySender, null, 400, "Missing Identifier.");
+                       return;
+               }
                if (!active.get()) {
-                       try {
-                               sendReply(pluginReplySender, null, new ErrorResponse(400, "FCP Interface deactivated"));
-                       } catch (PluginNotFoundException pnfe1) {
-                               logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
-                       }
+                       sendErrorReply(pluginReplySender, identifier, 503, "FCP Interface deactivated");
                        return;
                }
                AbstractSoneCommand command = commands.get(parameters.get("Message"));
-               if ((accessType == FredPluginFCP.ACCESS_FCP_RESTRICTED) && (((fullAccessRequired.get() == FullAccessRequired.WRITING) && command.requiresWriteAccess()) || (fullAccessRequired.get() == FullAccessRequired.ALWAYS))) {
-                       try {
-                               sendReply(pluginReplySender, null, new ErrorResponse(401, "Not authorized"));
-                       } catch (PluginNotFoundException pnfe1) {
-                               logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
-                       }
+               if (command == null) {
+                       sendErrorReply(pluginReplySender, identifier, 404, "Unrecognized Message: " + parameters.get("Message"));
+                       return;
+               }
+               if (!accessAuthorizer.authorized(AccessType.values()[accessType], fullAccessRequired.get(), command.requiresWriteAccess())) {
+                       sendErrorReply(pluginReplySender, identifier, 401, "Not authorized");
                        return;
                }
                try {
-                       if (command == null) {
-                               sendReply(pluginReplySender, null, new ErrorResponse("Unrecognized Message: " + parameters.get("Message")));
-                               return;
-                       }
-                       String identifier = parameters.get("Identifier");
-                       if ((identifier == null) || (identifier.length() == 0)) {
-                               sendReply(pluginReplySender, null, new ErrorResponse("Missing Identifier."));
-                               return;
-                       }
-                       try {
-                               Response response = command.execute(parameters, data, AccessType.values()[accessType]);
-                               sendReply(pluginReplySender, identifier, response);
-                       } catch (Exception e1) {
-                               logger.log(Level.WARNING, "Could not process FCP command “%s”.", command);
-                               sendReply(pluginReplySender, identifier, new ErrorResponse("Error executing command: " + e1.getMessage()));
-                       }
+                       Response response = command.execute(parameters);
+                       sendReply(pluginReplySender, identifier, response);
+               } catch (Exception e1) {
+                       logger.log(Level.WARNING, "Could not process FCP command “%s”.", command);
+                       sendErrorReply(pluginReplySender, identifier, 500, "Error executing command: " + e1.getMessage());
+               }
+       }
+
+       private void sendErrorReply(PluginReplySender pluginReplySender, String identifier, int errorCode, String message) {
+               try {
+                       sendReply(pluginReplySender, identifier, new ErrorResponse(errorCode, message));
                } catch (PluginNotFoundException pnfe1) {
-                       logger.log(Level.WARNING, "Could not find destination plugin: " + pluginReplySender);
+                       logger.log(Level.FINE, "Could not send error to plugin.", pnfe1);
                }
        }
 
@@ -212,13 +200,7 @@ public class FcpInterface {
                if (identifier != null) {
                        replyParameters.putOverwrite("Identifier", identifier);
                }
-               if (response.hasData()) {
-                       pluginReplySender.send(replyParameters, response.getData());
-               } else if (response.hasBucket()) {
-                       pluginReplySender.send(replyParameters, response.getBucket());
-               } else {
-                       pluginReplySender.send(replyParameters);
-               }
+               pluginReplySender.send(replyParameters);
        }
 
        @Subscribe
@@ -236,4 +218,38 @@ public class FcpInterface {
                setFullAccessRequired(fullAccessRequiredChanged.getFullAccessRequired());
        }
 
+       @Singleton
+       public static class CommandSupplier {
+
+               public Map<String, AbstractSoneCommand> supplyCommands(Core core) {
+                       Map<String, AbstractSoneCommand> commands = new HashMap<>();
+                       commands.put("Version", new VersionCommand(core));
+                       commands.put("GetLocalSones", new GetLocalSonesCommand(core));
+                       commands.put("GetSones", new GetSonesCommand(core));
+                       commands.put("GetSone", new GetSoneCommand(core));
+                       commands.put("GetPost", new GetPostCommand(core));
+                       commands.put("GetPosts", new GetPostsCommand(core));
+                       commands.put("GetPostFeed", new GetPostFeedCommand(core));
+                       commands.put("LockSone", new LockSoneCommand(core));
+                       commands.put("UnlockSone", new UnlockSoneCommand(core));
+                       commands.put("LikePost", new LikePostCommand(core));
+                       commands.put("LikeReply", new LikeReplyCommand(core));
+                       commands.put("CreatePost", new CreatePostCommand(core));
+                       commands.put("CreateReply", new CreateReplyCommand(core));
+                       commands.put("DeletePost", new DeletePostCommand(core));
+                       commands.put("DeleteReply", new DeleteReplyCommand(core));
+                       return commands;
+               }
+
+       }
+
+       @Singleton
+       public static class AccessAuthorizer {
+
+               public boolean authorized(@Nonnull AccessType accessType, @Nonnull FullAccessRequired fullAccessRequired, boolean commandRequiresWriteAccess) {
+                       return (accessType != RESTRICTED_FCP) || (fullAccessRequired == NO) || ((fullAccessRequired == WRITING) && !commandRequiresWriteAccess);
+               }
+
+       }
+
 }
index 8ac80cc..a55a81c 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.fcp;
 
 import net.pterodactylus.sone.core.Core;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “GetLocalSones” FCP command that returns the list of local
@@ -43,7 +42,7 @@ public class GetLocalSonesCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) {
+       public Response execute(SimpleFieldSet parameters) {
                return new Response("ListLocalSones", encodeSones(getCore().getLocalSones(), "LocalSones."));
        }
 
index 1e02dae..a6f1ae8 100644 (file)
@@ -21,7 +21,6 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * The “GetPost” FCP command returns a single {@link Post} to an FCP client.
@@ -44,7 +43,7 @@ public class GetPostCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Post post = getPost(parameters, "Post");
                boolean includeReplies = getBoolean(parameters, "IncludeReplies", true);
 
index dea5156..7b804fb 100644 (file)
@@ -32,7 +32,6 @@ import com.google.common.base.Optional;
 import com.google.common.collect.Collections2;
 
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implementation of an FCP interface for other clients or plugins to
@@ -56,7 +55,7 @@ public class GetPostFeedCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Sone sone = getSone(parameters, "Sone", true);
                int startPost = getInt(parameters, "StartPost", 0);
                int maxPosts = getInt(parameters, "MaxPosts", -1);
index 4059bf5..b9932a6 100644 (file)
@@ -25,7 +25,6 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “GetPosts” FCP command that returns the list of posts a Sone
@@ -49,7 +48,7 @@ public class GetPostsCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Sone sone = getSone(parameters, "Sone", false);
                int startPost = getInt(parameters, "StartPost", 0);
                int maxPosts = getInt(parameters, "MaxPosts", -1);
index f23be19..9d0e0b7 100644 (file)
@@ -25,7 +25,6 @@ import net.pterodactylus.sone.freenet.fcp.FcpException;
 import com.google.common.base.Optional;
 
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “GetSone“ FCP command which returns {@link Profile}
@@ -49,10 +48,10 @@ public class GetSoneCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Sone sone = getSone(parameters, "Sone", false);
-               Optional<Sone> localSone = getSone(parameters, "LocalSone", false, false);
-               return new Response("Sone", encodeSone(sone, "", localSone));
+               Optional<Sone> localSone = getSone(parameters, "LocalSone", true, false);
+               return new Response("Sone", encodeSone(sone, "Sone.", localSone));
        }
 
 }
index 97c8842..fa805e1 100644 (file)
@@ -24,7 +24,6 @@ import java.util.List;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “GetSones” FCP command that returns the list of known Sones.
@@ -47,15 +46,15 @@ public class GetSonesCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) {
+       public Response execute(SimpleFieldSet parameters) {
                int startSone = getInt(parameters, "StartSone", 0);
                int maxSones = getInt(parameters, "MaxSones", -1);
                List<Sone> sones = new ArrayList<Sone>(getCore().getSones());
                if (sones.size() < startSone) {
-                       return new Response("Sones", encodeSones(Collections.<Sone> emptyList(), ""));
+                       return new Response("Sones", encodeSones(Collections.<Sone> emptyList(), "Sones."));
                }
                Collections.sort(sones, Sone.NICE_NAME_COMPARATOR);
-               return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), ""));
+               return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), "Sones."));
        }
 
 }
index 583b8e4..a2fdd18 100644 (file)
@@ -23,7 +23,6 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “LikePost” FCP command which allows the user to like a post.
@@ -46,7 +45,7 @@ public class LikePostCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Post post = getPost(parameters, "Post");
                Sone sone = getSone(parameters, "Sone", true);
                sone.addLikedPostId(post.getId());
index 63d8893..6fba11b 100644 (file)
@@ -23,7 +23,6 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implements the “LikeReply” FCP command which allows the user to like a reply.
@@ -46,7 +45,7 @@ public class LikeReplyCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                PostReply reply = getReply(parameters, "Reply");
                Sone sone = getSone(parameters, "Sone", true);
                sone.addLikedReplyId(reply.getId());
index e05ac93..f39a18c 100644 (file)
@@ -23,7 +23,6 @@ import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 import com.google.common.base.Optional;
 
@@ -51,7 +50,7 @@ public class LockSoneCommand extends AbstractSoneCommand {
        //
 
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Optional<Sone> sone = getSone(parameters, "Sone", true, true);
                getCore().lockSone(sone.get());
                return new Response("SoneLocked", new SimpleFieldSetBuilder().put("Sone", sone.get().getId()).get());
index 311a7ca..bc78441 100644 (file)
@@ -23,7 +23,6 @@ import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.freenet.fcp.FcpException;
 
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 import com.google.common.base.Optional;
 
@@ -51,7 +50,7 @@ public class UnlockSoneCommand extends AbstractSoneCommand {
        //
 
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+       public Response execute(SimpleFieldSet parameters) throws FcpException {
                Optional<Sone> sone = getSone(parameters, "Sone", true, true);
                getCore().unlockSone(sone.get());
                return new Response("SoneUnlocked", new SimpleFieldSetBuilder().put("Sone", sone.get().getId()).get());
index ee0f2e5..5f50b4d 100644 (file)
@@ -21,7 +21,6 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import net.pterodactylus.sone.main.SonePlugin;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Returns version information about the Sone plugin.
@@ -44,7 +43,7 @@ public class VersionCommand extends AbstractSoneCommand {
         * {@inheritDoc}
         */
        @Override
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) {
+       public Response execute(SimpleFieldSet parameters) {
                return new Response("Version", new SimpleFieldSetBuilder().put("Version", SonePlugin.getPluginVersion()).put("ProtocolVersion", 1).get());
        }
 
index 3b4af2a..7ca9a5c 100644 (file)
@@ -23,6 +23,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
@@ -53,8 +55,23 @@ public class L10nFilter implements Filter {
         */
        @Override
        public String format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
-               if (parameters.isEmpty()) {
-                       return webInterface.getL10n().getString(String.valueOf(data));
+               List<Object> parameterValues = getParameters(data, parameters);
+               String text = getText(data);
+               if (parameterValues.isEmpty()) {
+                       return webInterface.getL10n().getString(text);
+               }
+               return new MessageFormat(webInterface.getL10n().getString(text), new Locale(webInterface.getL10n().getSelectedLanguage().shortCode)).format(parameterValues.toArray());
+       }
+
+       @Nonnull
+       private String getText(Object data) {
+               return (data instanceof L10nText) ? ((L10nText) data).getText() : String.valueOf(data);
+       }
+
+       @Nonnull
+       private List<Object> getParameters(Object data, Map<String, Object> parameters) {
+               if (data instanceof L10nText) {
+                       return ((L10nText) data).getParameters();
                }
                List<Object> parameterValues = new ArrayList<Object>();
                int parameterIndex = 0;
@@ -63,7 +80,7 @@ public class L10nFilter implements Filter {
                        parameterValues.add(value);
                        ++parameterIndex;
                }
-               return new MessageFormat(webInterface.getL10n().getString(String.valueOf(data)), new Locale(webInterface.getL10n().getSelectedLanguage().shortCode)).format(parameterValues.toArray());
+               return parameterValues;
        }
 
 }
index bafa764..17f8370 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.freenet.fcp;
 
 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
 import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
 
 /**
  * Implementation of an FCP interface for other clients or plugins to
@@ -35,15 +34,11 @@ public interface Command {
         *
         * @param parameters
         *            The parameters of the comand
-        * @param data
-        *            The data of the command (may be {@code null})
-        * @param accessType
-        *            The access type
         * @return A reply to send back to the plugin
         * @throws FcpException
         *             if an error processing the parameters occurs
         */
-       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException;
+       public Response execute(SimpleFieldSet parameters) throws FcpException;
 
        /**
         * The access type of the request.
@@ -76,12 +71,6 @@ public interface Command {
                /** The reply parameters. */
                private final SimpleFieldSet replyParameters;
 
-               /** The reply data, may be {@code null}. */
-               private final byte[] data;
-
-               /** The data bucket, may be {@code null}. */
-               private final Bucket bucket;
-
                /**
                 * Creates a new reply with the given parameters.
                 *
@@ -91,54 +80,8 @@ public interface Command {
                 *            The reply parameters
                 */
                public Response(String messageName, SimpleFieldSet replyParameters) {
-                       this(messageName, replyParameters, null, null);
-               }
-
-               /**
-                * Creates a new reply with the given parameters.
-                *
-                * @param messageName
-                *            The message name
-                * @param replyParameters
-                *            The reply parameters
-                * @param data
-                *            The data of the reply (may be {@code null})
-                */
-               public Response(String messageName, SimpleFieldSet replyParameters, byte[] data) {
-                       this(messageName, replyParameters, data, null);
-               }
-
-               /**
-                * Creates a new reply with the given parameters.
-                *
-                * @param messageName
-                *            The message name
-                * @param replyParameters
-                *            The reply parameters
-                * @param bucket
-                *            The bucket of the reply (may be {@code null})
-                */
-               public Response(String messageName, SimpleFieldSet replyParameters, Bucket bucket) {
-                       this(messageName, replyParameters, null, bucket);
-               }
-
-               /**
-                * Creates a new reply with the given parameters.
-                *
-                * @param messageName
-                *            The message name
-                * @param replyParameters
-                *            The reply parameters
-                * @param data
-                *            The data of the reply (may be {@code null})
-                * @param bucket
-                *            The bucket of the reply (may be {@code null})
-                */
-               private Response(String messageName, SimpleFieldSet replyParameters, byte[] data, Bucket bucket) {
                        this.messageName = messageName;
                        this.replyParameters = replyParameters;
-                       this.data = data;
-                       this.bucket = bucket;
                }
 
                /**
@@ -150,45 +93,6 @@ public interface Command {
                        return new SimpleFieldSetBuilder(replyParameters).put("Message", messageName).get();
                }
 
-               /**
-                * Returns whether the reply has reply data.
-                *
-                * @see #getData()
-                * @return {@code true} if this reply has data, {@code false} otherwise
-                */
-               public boolean hasData() {
-                       return data != null;
-               }
-
-               /**
-                * Returns the data of the reply.
-                *
-                * @return The data of the reply
-                */
-               public byte[] getData() {
-                       return data;
-               }
-
-               /**
-                * Returns whether the reply has a data bucket.
-                *
-                * @see #getBucket()
-                * @return {@code true} if the reply has a data bucket, {@code false}
-                *         otherwise
-                */
-               public boolean hasBucket() {
-                       return bucket != null;
-               }
-
-               /**
-                * Returns the data bucket of the reply.
-                *
-                * @return The data bucket of the reply
-                */
-               public Bucket getBucket() {
-                       return bucket;
-               }
-
        }
 
        /**
index cfce632..f014de5 100644 (file)
@@ -105,7 +105,7 @@ public class IdentityChangeDetector {
                return new Predicate<Identity>() {
                        @Override
                        public boolean apply(Identity identity) {
-                               return (identity == null) ? false : identityHasChanged(oldIdentities.get(identity.getId()), identity);
+                               return (identity != null) && identityHasChanged(oldIdentities.get(identity.getId()), identity);
                        }
                };
        }
@@ -151,7 +151,7 @@ public class IdentityChangeDetector {
                return new Predicate<String>() {
                        @Override
                        public boolean apply(String context) {
-                               return (identity == null) ? false : !identity.getContexts().contains(context);
+                               return (identity != null) && !identity.getContexts().contains(context);
                        }
                };
        }
@@ -160,7 +160,7 @@ public class IdentityChangeDetector {
                return new Predicate<Identity>() {
                        @Override
                        public boolean apply(Identity identity) {
-                               return (identity == null) ? false : !newIdentities.contains(identity);
+                               return (identity != null) && !newIdentities.contains(identity);
                        }
                };
        }
@@ -169,7 +169,7 @@ public class IdentityChangeDetector {
                return new Predicate<Entry<String, String>>() {
                        @Override
                        public boolean apply(Entry<String, String> property) {
-                               return (property == null) ? false : !identity.getProperties().containsKey(property.getKey());
+                               return (property != null) && !identity.getProperties().containsKey(property.getKey());
                        }
                };
        }
@@ -178,7 +178,7 @@ public class IdentityChangeDetector {
                return new Predicate<Entry<String, String>>() {
                        @Override
                        public boolean apply(Entry<String, String> property) {
-                               return (property == null) ? false : !newIdentity.getProperty(property.getKey()).equals(property.getValue());
+                               return (property != null) && !newIdentity.getProperty(property.getKey()).equals(property.getValue());
                        }
                };
        }
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 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;
index 61bd6ce..c1628eb 100644 (file)
@@ -54,7 +54,6 @@ import com.google.inject.spi.TypeListener;
 import freenet.client.async.PersistenceDisabledException;
 import freenet.l10n.BaseL10n.LANGUAGE;
 import freenet.l10n.PluginL10n;
-import freenet.node.Node;
 import freenet.pluginmanager.FredPlugin;
 import freenet.pluginmanager.FredPluginBaseL10n;
 import freenet.pluginmanager.FredPluginFCP;
@@ -115,13 +114,10 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                });
        }
 
-       /** The version. */
-       private static final Version VERSION = new Version(0, 9, 6);
-
        /** The current year at time of release. */
-       private static final int YEAR = 2016;
+       private static final int YEAR = 2017;
        private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
-       private static final int LATEST_EDITION = 73;
+       private static final int LATEST_EDITION = 76;
 
        /** The logger. */
        private static final Logger logger = getLogger(SonePlugin.class.getName());
@@ -176,7 +172,8 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        public static String getPluginVersion() {
-               return VERSION.toString();
+               net.pterodactylus.sone.main.Version version = VersionParserKt.getParsedVersion();
+               return (version == null) ? "unknown" : version.getNice();
        }
 
        public static int getYear() {
@@ -238,15 +235,8 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                final EventBus eventBus = new EventBus();
 
                /* Freenet injector configuration. */
-               AbstractModule freenetModule = new AbstractModule() {
+               FreenetModule freenetModule =  new FreenetModule(pluginRespirator);
 
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       protected void configure() {
-                               bind(PluginRespirator.class).toInstance(SonePlugin.this.pluginRespirator);
-                               bind(Node.class).toInstance(SonePlugin.this.pluginRespirator.getNode());
-                       }
-               };
                /* Sone injector configuration. */
                AbstractModule soneModule = new AbstractModule() {
 
@@ -258,7 +248,10 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                                bind(Context.class).toInstance(context);
                                bind(getOptionalContextTypeLiteral()).toInstance(of(context));
                                bind(SonePlugin.class).toInstance(SonePlugin.this);
-                               bind(Version.class).toInstance(VERSION);
+                               bind(Version.class).toInstance(Version.parse(getVersion()));
+                               bind(PluginVersion.class).toInstance(new PluginVersion(getVersion()));
+                               bind(PluginYear.class).toInstance(new PluginYear(getYear()));
+                               bind(PluginHomepage.class).toInstance(new PluginHomepage(getHomepage()));
                                if (startConfiguration.getBooleanValue("Developer.LoadFromFilesystem").getValue(false)) {
                                        String path = startConfiguration.getStringValue("Developer.FilesystemPath").getValue(null);
                                        if (path != null) {
@@ -409,7 +402,49 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         */
        @Override
        public String getVersion() {
-               return VERSION.toString();
+               return getPluginVersion();
+       }
+
+       public static class PluginVersion {
+
+               private final String version;
+
+               public PluginVersion(String version) {
+                       this.version = version;
+               }
+
+               public String getVersion() {
+                       return version;
+               }
+
+       }
+
+       public static class PluginYear {
+
+               private final int year;
+
+               public PluginYear(int year) {
+                       this.year = year;
+               }
+
+               public int getYear() {
+                       return year;
+               }
+
+       }
+
+       public static class PluginHomepage {
+
+               private final String homepage;
+
+               public PluginHomepage(String homepage) {
+                       this.homepage = homepage;
+               }
+
+               public String getHomepage() {
+                       return homepage;
+               }
+
        }
 
 }
index 473c191..ad753a7 100644 (file)
@@ -2,7 +2,7 @@ package net.pterodactylus.sone.template;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -18,8 +18,6 @@ import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateException;
 import net.pterodactylus.util.template.TemplateParser;
 
-import freenet.support.io.Closer;
-
 import com.google.common.base.Charsets;
 
 /**
@@ -46,28 +44,19 @@ public class FilesystemTemplate extends Template {
 
        private void loadTemplate() {
                File templateFile = new File(filename);
-               if (!templateFile.exists()) {
-                       throw new TemplateFileNotFoundException(filename);
-               }
                if (templateWasLoaded() && !templateFileHasBeenModifiedAfterLoading(templateFile)) {
                        return;
                }
-               InputStream templateInputStream = null;
-               Reader templateReader = null;
-               try {
-                       templateInputStream = new FileInputStream(templateFile);
-                       templateReader = new InputStreamReader(templateInputStream, Charsets.UTF_8);
+               try (InputStream templateInputStream = new FileInputStream(templateFile);
+                               Reader templateReader = new InputStreamReader(templateInputStream, Charsets.UTF_8)) {
                        Template template = TemplateParser.parse(templateReader);
                        lastTemplate.set(new LastLoadedTemplate(template));
                        template.getInitialContext().mergeContext(initialContext);
                        for (Part part : parts) {
                                template.add(part);
                        }
-               } catch (FileNotFoundException e) {
+               } catch (IOException e) {
                        throw new TemplateFileNotFoundException(filename);
-               } finally {
-                       Closer.close(templateReader);
-                       Closer.close(templateInputStream);
                }
        }
 
index 74c1a77..7766af3 100644 (file)
@@ -19,6 +19,9 @@ package net.pterodactylus.sone.template;
 
 import java.util.Set;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
@@ -32,6 +35,7 @@ import net.pterodactylus.util.template.TemplateContext;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
+@Singleton
 public class IdentityAccessor extends ReflectionAccessor {
 
        /** The core. */
@@ -43,6 +47,7 @@ public class IdentityAccessor extends ReflectionAccessor {
         * @param core
         *            The core
         */
+       @Inject
        public IdentityAccessor(Core core) {
                this.core = core;
        }
diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java
deleted file mode 100644 (file)
index 7cf8c70..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Sone - ParserFilter.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.template;
-
-import static java.lang.String.valueOf;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
-
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.FreenetLinkPart;
-import net.pterodactylus.sone.text.LinkPart;
-import net.pterodactylus.sone.text.Part;
-import net.pterodactylus.sone.text.PlainTextPart;
-import net.pterodactylus.sone.text.PostPart;
-import net.pterodactylus.sone.text.SonePart;
-import net.pterodactylus.sone.text.SoneTextParser;
-import net.pterodactylus.sone.text.SoneTextParserContext;
-import net.pterodactylus.util.template.Filter;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateParser;
-
-/**
- * Filter that filters a given text through a {@link SoneTextParser}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ParserFilter implements Filter {
-
-       /** The core. */
-       private final Core core;
-
-       /** The link parser. */
-       private final SoneTextParser soneTextParser;
-
-       /** The template context factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /** The template for {@link PlainTextPart}s. */
-       private static final Template plainTextTemplate = TemplateParser.parse(new StringReader("<%text|html>"));
-
-       /** The template for {@link FreenetLinkPart}s. */
-       private static final Template linkTemplate = TemplateParser.parse(new StringReader("<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>"));
-
-       /**
-        * Creates a new filter that runs its input through a {@link SoneTextParser}
-        * .
-        *
-        * @param core
-        *            The core
-        * @param templateContextFactory
-        *            The context factory for rendering the parts
-        * @param soneTextParser
-        *            The Sone text parser
-        */
-       public ParserFilter(Core core, TemplateContextFactory templateContextFactory, SoneTextParser soneTextParser) {
-               this.core = core;
-               this.templateContextFactory = templateContextFactory;
-               this.soneTextParser = soneTextParser;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Object format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
-               String text = valueOf(data);
-               int length = parseInt(valueOf(parameters.get("length")), -1);
-               int cutOffLength = parseInt(valueOf(parameters.get("cut-off-length")), length);
-               Object sone = parameters.get("sone");
-               if (sone instanceof String) {
-                       sone = core.getSone((String) sone).orNull();
-               }
-               SoneTextParserContext context = new SoneTextParserContext((Sone) sone);
-               StringWriter parsedTextWriter = new StringWriter();
-               Iterable<Part> parts = soneTextParser.parse(text, context);
-               if (length > -1) {
-                       int allPartsLength = 0;
-                       List<Part> shortenedParts = new ArrayList<Part>();
-                       for (Part part : parts) {
-                               if (part instanceof PlainTextPart) {
-                                       String longText = part.getText();
-                                       if (allPartsLength < cutOffLength) {
-                                               if ((allPartsLength + longText.length()) > cutOffLength) {
-                                                       shortenedParts.add(new PlainTextPart(longText.substring(0, cutOffLength - allPartsLength) + "…"));
-                                               } else {
-                                                       shortenedParts.add(part);
-                                               }
-                                       }
-                                       allPartsLength += longText.length();
-                               } else if (part instanceof LinkPart) {
-                                       if (allPartsLength < cutOffLength) {
-                                               shortenedParts.add(part);
-                                       }
-                                       allPartsLength += part.getText().length();
-                               } else {
-                                       if (allPartsLength < cutOffLength) {
-                                               shortenedParts.add(part);
-                                       }
-                               }
-                       }
-                       if (allPartsLength >= length) {
-                               parts = shortenedParts;
-                       }
-               }
-               render(parsedTextWriter, parts);
-               return parsedTextWriter.toString();
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Renders the given parts.
-        *
-        * @param writer
-        *            The writer to render the parts to
-        * @param parts
-        *            The parts to render
-        */
-       private void render(Writer writer, Iterable<Part> parts) {
-               for (Part part : parts) {
-                       render(writer, part);
-               }
-       }
-
-       /**
-        * Renders the given part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param part
-        *            The part to render
-        */
-       @SuppressWarnings("unchecked")
-       private void render(Writer writer, Part part) {
-               if (part instanceof PlainTextPart) {
-                       render(writer, (PlainTextPart) part);
-               } else if (part instanceof FreenetLinkPart) {
-                       render(writer, (FreenetLinkPart) part);
-               } else if (part instanceof LinkPart) {
-                       render(writer, (LinkPart) part);
-               } else if (part instanceof SonePart) {
-                       render(writer, (SonePart) part);
-               } else if (part instanceof PostPart) {
-                       render(writer, (PostPart) part);
-               } else if (part instanceof Iterable<?>) {
-                       render(writer, (Iterable<Part>) part);
-               }
-       }
-
-       /**
-        * Renders the given plain-text part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param plainTextPart
-        *            The part to render
-        */
-       private void render(Writer writer, PlainTextPart plainTextPart) {
-               TemplateContext templateContext = templateContextFactory.createTemplateContext();
-               templateContext.set("text", plainTextPart.getText());
-               plainTextTemplate.render(templateContext, writer);
-       }
-
-       /**
-        * Renders the given freenet link part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param freenetLinkPart
-        *            The part to render
-        */
-       private void render(Writer writer, FreenetLinkPart freenetLinkPart) {
-               renderLink(writer, "/" + freenetLinkPart.getLink(), freenetLinkPart.getText(), freenetLinkPart.getTitle(), freenetLinkPart.isTrusted() ? "freenet-trusted" : "freenet");
-       }
-
-       /**
-        * Renders the given link part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param linkPart
-        *            The part to render
-        */
-       private void render(Writer writer, LinkPart linkPart) {
-               try {
-                       renderLink(writer, "/external-link/?_CHECKED_HTTP_=" + URLEncoder.encode(linkPart.getLink(), "UTF-8"), linkPart.getText(), linkPart.getTitle(), "internet");
-               } catch (UnsupportedEncodingException uee1) {
-                       /* not possible for UTF-8. */
-                       throw new RuntimeException("The JVM does not support UTF-8 encoding!", uee1);
-               }
-       }
-
-       /**
-        * Renders the given Sone part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param sonePart
-        *            The part to render
-        */
-       private void render(Writer writer, SonePart sonePart) {
-               if ((sonePart.getSone() != null) && (sonePart.getSone().getName() != null)) {
-                       renderLink(writer, "viewSone.html?sone=" + sonePart.getSone().getId(), SoneAccessor.getNiceName(sonePart.getSone()), SoneAccessor.getNiceName(sonePart.getSone()), "in-sone");
-               } else {
-                       renderLink(writer, "/WebOfTrust/ShowIdentity?id=" + sonePart.getSone().getId(), sonePart.getSone().getId(), sonePart.getSone().getId(), "in-sone");
-               }
-       }
-
-       /**
-        * Renders the given post part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param postPart
-        *            The part to render
-        */
-       private void render(Writer writer, PostPart postPart) {
-               SoneTextParser parser = new SoneTextParser(core, core);
-               SoneTextParserContext parserContext = new SoneTextParserContext(postPart.getPost().getSone());
-               Iterable<Part> parts = parser.parse(postPart.getPost().getText(), parserContext);
-               StringBuilder excerpt = new StringBuilder();
-               for (Part part : parts) {
-                       excerpt.append(part.getText());
-                       if (excerpt.length() > 20) {
-                               int lastSpace = excerpt.lastIndexOf(" ", 20);
-                               if (lastSpace > -1) {
-                                       excerpt.setLength(lastSpace);
-                               } else {
-                                       excerpt.setLength(20);
-                               }
-                               excerpt.append("…");
-                               break;
-                       }
-               }
-               renderLink(writer, "viewPost.html?post=" + postPart.getPost().getId(), excerpt.toString(), SoneAccessor.getNiceName(postPart.getPost().getSone()), "in-sone");
-       }
-
-       /**
-        * Renders the given link.
-        *
-        * @param writer
-        *            The writer to render the link to
-        * @param link
-        *            The link to render
-        * @param text
-        *            The text of the link
-        * @param title
-        *            The title of the link
-        * @param cssClass
-        *            The CSS class of the link
-        */
-       private void renderLink(Writer writer, String link, String text, String title, String cssClass) {
-               TemplateContext templateContext = templateContextFactory.createTemplateContext();
-               templateContext.set("cssClass", cssClass);
-               templateContext.set("link", link);
-               templateContext.set("text", text);
-               templateContext.set("title", title);
-               linkTemplate.render(templateContext, writer);
-       }
-
-}
index 980d960..fafba86 100644 (file)
@@ -20,7 +20,7 @@ package net.pterodactylus.sone.template;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.util.template.Accessor;
@@ -74,24 +74,24 @@ public class ProfileAccessor extends ReflectionAccessor {
                                /* always show your own avatars. */
                                return avatarId;
                        }
-                       ShowCustomAvatars showCustomAvatars = currentSone.getOptions().getShowCustomAvatars();
-                       if (showCustomAvatars == ShowCustomAvatars.NEVER) {
+                       LoadExternalContent showCustomAvatars = currentSone.getOptions().getShowCustomAvatars();
+                       if (showCustomAvatars == LoadExternalContent.NEVER) {
                                return null;
                        }
-                       if (showCustomAvatars == ShowCustomAvatars.ALWAYS) {
+                       if (showCustomAvatars == LoadExternalContent.ALWAYS) {
                                return avatarId;
                        }
-                       if (showCustomAvatars == ShowCustomAvatars.FOLLOWED) {
+                       if (showCustomAvatars == LoadExternalContent.FOLLOWED) {
                                return currentSone.hasFriend(remoteSone.getId()) ? avatarId : null;
                        }
                        Trust trust = remoteSone.getIdentity().getTrust((OwnIdentity) currentSone.getIdentity());
                        if (trust == null) {
                                return null;
                        }
-                       if ((showCustomAvatars == ShowCustomAvatars.MANUALLY_TRUSTED) && (trust.getExplicit() != null) && (trust.getExplicit() > 0)) {
+                       if ((showCustomAvatars == LoadExternalContent.MANUALLY_TRUSTED) && (trust.getExplicit() != null) && (trust.getExplicit() > 0)) {
                                return avatarId;
                        }
-                       if ((showCustomAvatars == ShowCustomAvatars.TRUSTED) && (((trust.getExplicit() != null) && (trust.getExplicit() > 0)) || ((trust.getImplicit() != null) && (trust.getImplicit() > 0)))) {
+                       if ((showCustomAvatars == LoadExternalContent.TRUSTED) && (((trust.getExplicit() != null) && (trust.getExplicit() > 0)) || ((trust.getImplicit() != null) && (trust.getImplicit() > 0)))) {
                                return avatarId;
                        }
                        return null;
index 9bb5b81..da12ca8 100644 (file)
@@ -30,8 +30,6 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
 
-import com.google.common.base.Optional;
-
 /**
  * {@link Filter} implementation that groups replies by the post the are in
  * reply to, returning a map with the post as key and the list of replies as
@@ -48,33 +46,30 @@ public class ReplyGroupFilter implements Filter {
        public Object format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
                @SuppressWarnings("unchecked")
                List<PostReply> allReplies = (List<PostReply>) data;
-               Map<Post, Set<Sone>> postSones = new HashMap<Post, Set<Sone>>();
-               Map<Post, Set<PostReply>> postReplies = new HashMap<Post, Set<PostReply>>();
+               Map<Post, Set<Sone>> postSones = new HashMap<>();
+               Map<Post, Set<PostReply>> postReplies = new HashMap<>();
                for (PostReply reply : allReplies) {
                        /*
                         * All replies from a new-reply notification have posts,
                         * ListNotificationFilters takes care of that.
                         */
-                       Optional<Post> post = reply.getPost();
-                       Set<Sone> sones = postSones.get(post.get());
+                       Post post = reply.getPost().get();
+                       Set<Sone> sones = postSones.get(post);
                        if (sones == null) {
-                               sones = new HashSet<Sone>();
-                               postSones.put(post.get(), sones);
+                               sones = new HashSet<>();
+                               postSones.put(post, sones);
                        }
                        sones.add(reply.getSone());
-                       Set<PostReply> replies = postReplies.get(post.get());
+                       Set<PostReply> replies = postReplies.get(post);
                        if (replies == null) {
-                               replies = new HashSet<PostReply>();
-                               postReplies.put(post.get(), replies);
+                               replies = new HashSet<>();
+                               postReplies.put(post, replies);
                        }
                        replies.add(reply);
                }
-               Map<Post, Map<String, Set<?>>> result = new HashMap<Post, Map<String, Set<?>>>();
+               Map<Post, Map<String, Set<?>>> result = new HashMap<>();
                for (Entry<Post, Set<Sone>> postEntry : postSones.entrySet()) {
-                       if (result.containsKey(postEntry.getKey())) {
-                               continue;
-                       }
-                       Map<String, Set<?>> postResult = new HashMap<String, Set<?>>();
+                       Map<String, Set<?>> postResult = new HashMap<>();
                        postResult.put("sones", postEntry.getValue());
                        postResult.put("replies", postReplies.get(postEntry.getKey()));
                        result.put(postEntry.getKey(), postResult);
index 50fa272..5f4ab0c 100644 (file)
@@ -32,8 +32,7 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.freenet.wot.Trust;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
+import net.pterodactylus.sone.text.TimeTextConverter;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.TemplateContext;
@@ -62,6 +61,7 @@ public class SoneAccessor extends ReflectionAccessor {
 
        /** The core. */
        private final Core core;
+       private final TimeTextConverter timeTextConverter;
 
        /**
         * Creates a new Sone accessor.
@@ -69,8 +69,9 @@ public class SoneAccessor extends ReflectionAccessor {
         * @param core
         *            The Sone core
         */
-       public SoneAccessor(Core core) {
+       public SoneAccessor(Core core, TimeTextConverter timeTextConverter) {
                this.core = core;
+               this.timeTextConverter = timeTextConverter;
        }
 
        /**
@@ -104,7 +105,7 @@ public class SoneAccessor extends ReflectionAccessor {
                } else if (member.equals("locked")) {
                        return core.isLocked(sone);
                } else if (member.equals("lastUpdatedText")) {
-                       return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), sone.getTime());
+                       return timeTextConverter.getTimeText(sone.getTime()).getL10nText();
                } else if (member.equals("trust")) {
                        Sone currentSone = (Sone) templateContext.get("currentSone");
                        if (currentSone == null) {
index 2104888..f14d0ff 100644 (file)
@@ -48,7 +48,7 @@ public class SubstringFilter implements Filter {
                }
                String dataString = String.valueOf(data);
                int dataLength = dataString.length();
-               int length = Integer.MAX_VALUE;
+               int length = dataLength;
                try {
                        length = Integer.parseInt(lengthString);
                } catch (NumberFormatException nfe1) {
index fee047d..32650f1 100644 (file)
@@ -43,10 +43,6 @@ public class TrustAccessor extends ReflectionAccessor {
                Trust trust = (Trust) object;
                if ("assigned".equals(member)) {
                        return trust.getExplicit() != null;
-               } else if ("maximum".equals(member)) {
-                       return ((trust.getExplicit() != null) && (trust.getExplicit() >= 100)) || ((trust.getImplicit() != null) && (trust.getImplicit() >= 100));
-               } else if ("hasDistance".equals(member)) {
-                       return (trust.getDistance() != null) && (trust.getDistance() != Integer.MAX_VALUE);
                }
                return super.get(templateContext, object, member);
        }
diff --git a/src/main/java/net/pterodactylus/sone/text/FreemailPart.java b/src/main/java/net/pterodactylus/sone/text/FreemailPart.java
new file mode 100644 (file)
index 0000000..69e6bfe
--- /dev/null
@@ -0,0 +1,37 @@
+package net.pterodactylus.sone.text;
+
+/**
+ * {@link Part} implementation that holds a freemail address.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreemailPart implements Part {
+
+       private final String emailLocalPart;
+       private final String freemailId;
+       private final String identityId;
+
+       public FreemailPart(String emailLocalPart, String freemailId, String identityId) {
+               this.emailLocalPart = emailLocalPart;
+               this.freemailId = freemailId;
+               this.identityId = identityId;
+       }
+
+       @Override
+       public String getText() {
+               return String.format("%s@%s.freemail", emailLocalPart, freemailId);
+       }
+
+       public String getEmailLocalPart() {
+               return emailLocalPart;
+       }
+
+       public String getFreemailId() {
+               return freemailId;
+       }
+
+       public String getIdentityId() {
+               return identityId;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java
deleted file mode 100644 (file)
index 493880f..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Sone - FreenetLinkPart.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.text;
-
-import javax.annotation.Nonnull;
-
-/**
- * {@link LinkPart} implementation that stores an additional attribute: if the
- * link is an SSK or USK link and the post was created by an identity that owns
- * the keyspace in question.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkPart extends LinkPart {
-
-       private final boolean trusted;
-
-       public FreenetLinkPart(@Nonnull String link, @Nonnull String text, boolean trusted) {
-               this(link, text, text, trusted);
-       }
-
-       public FreenetLinkPart(@Nonnull String link, @Nonnull String text, @Nonnull String title, boolean trusted) {
-               super(link, text, title);
-               this.trusted = trusted;
-       }
-
-       public boolean isTrusted() {
-               return trusted;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/LinkPart.java b/src/main/java/net/pterodactylus/sone/text/LinkPart.java
deleted file mode 100644 (file)
index 9f889ef..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - LinkPart.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.text;
-
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-/**
- * {@link Part} implementation that can hold a link. A link contains of three
- * attributes: the link itself, the text that is shown instead of the link, and
- * an explanatory text that can be displayed e.g. as a tooltip.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LinkPart implements Part {
-
-       private final String link;
-       private final String text;
-       private final String title;
-
-       public LinkPart(@Nonnull String link, @Nonnull String text) {
-               this(link, text, text);
-       }
-
-       public LinkPart(@Nonnull String link, @Nonnull String text, @Nonnull String title) {
-               this.link = Objects.requireNonNull(link);
-               this.text = Objects.requireNonNull(text);
-               this.title = Objects.requireNonNull(title);
-       }
-
-       @Nonnull
-       public String getLink() {
-               return link;
-       }
-
-       @Nonnull
-       public String getTitle() {
-               return title;
-       }
-
-       @Override
-       @Nonnull
-       public String getText() {
-               return text;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/Part.java b/src/main/java/net/pterodactylus/sone/text/Part.java
deleted file mode 100644 (file)
index e516f0a..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Sone - Part.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.text;
-
-/**
- * A part is a single piece of information that can be displayed as a single
- * element. How the part is displayed is not part of the {@code Part}
- * specification.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Part {
-
-       /**
-        * Returns the text contained in this part. This should return plain text
-        * without any format information.
-        *
-        * @return The plain text of this part
-        */
-       public String getText();
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/PartContainer.java b/src/main/java/net/pterodactylus/sone/text/PartContainer.java
deleted file mode 100644 (file)
index 3444d19..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Sone - PartContainer.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.text;
-
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-/**
- * Part implementation that can contain an arbitrary amount of other parts.
- * Parts are added using the {@link #add(Part)} method and will be rendered in
- * the order they are added.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PartContainer implements Part, Iterable<Part> {
-
-       private final List<Part> parts = new ArrayList<Part>();
-
-       public void add(@Nonnull Part part) {
-               parts.add(Objects.requireNonNull(part));
-       }
-
-       @Nonnull
-       public Part getPart(int index) {
-               return parts.get(index);
-       }
-
-       public void removePart(int index) {
-               parts.remove(index);
-       }
-
-       public int size() {
-               return parts.size();
-       }
-
-       @Override
-       @Nonnull
-       public String getText() {
-               StringBuilder partText = new StringBuilder();
-               for (Part part : parts) {
-                       partText.append(part.getText());
-               }
-               return partText.toString();
-       }
-
-       @Override
-       @Nonnull
-       @SuppressWarnings("synthetic-access")
-       public Iterator<Part> iterator() {
-               return new Iterator<Part>() {
-
-                       private Deque<Iterator<Part>> partStack = new ArrayDeque<Iterator<Part>>();
-                       private Part nextPart;
-                       private boolean foundNextPart;
-                       private boolean noNextPart;
-
-                       {
-                               partStack.push(parts.iterator());
-                       }
-
-                       private void findNext() {
-                               if (foundNextPart) {
-                                       return;
-                               }
-                               noNextPart = true;
-                               while (!partStack.isEmpty()) {
-                                       Iterator<Part> parts = partStack.pop();
-                                       if (parts.hasNext()) {
-                                               nextPart = parts.next();
-                                               partStack.push(parts);
-                                               if (nextPart instanceof PartContainer) {
-                                                       partStack.push(((PartContainer) nextPart).iterator());
-                                               } else {
-                                                       noNextPart = false;
-                                                       break;
-                                               }
-                                       }
-                               }
-                               foundNextPart = true;
-                       }
-
-                       @Override
-                       public boolean hasNext() {
-                               findNext();
-                               return !noNextPart;
-                       }
-
-                       @Override
-                       public Part next() {
-                               findNext();
-                               if (noNextPart) {
-                                       throw new NoSuchElementException();
-                               }
-                               foundNextPart = false;
-                               return nextPart;
-                       }
-
-                       @Override
-                       public void remove() {
-                               /* ignore. */
-                       }
-
-               };
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/PlainTextPart.java b/src/main/java/net/pterodactylus/sone/text/PlainTextPart.java
deleted file mode 100644 (file)
index 7bdbee9..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Sone - PlainTextPart.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.text;
-
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-/**
- * {@link Part} implementation that holds a single piece of text.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PlainTextPart implements Part {
-
-       private final String text;
-
-       public PlainTextPart(@Nonnull String text) {
-               this.text = Objects.requireNonNull(text);
-       }
-
-       @Override
-       @Nonnull
-       public String getText() {
-               return text;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/SonePart.java b/src/main/java/net/pterodactylus/sone/text/SonePart.java
deleted file mode 100644 (file)
index 576eb7b..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Sone - SonePart.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.text;
-
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.template.SoneAccessor;
-
-/**
- * {@link Part} implementation that stores a reference to a {@link Sone}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class SonePart implements Part {
-
-       private final Sone sone;
-
-       public SonePart(@Nonnull Sone sone) {
-               this.sone = Objects.requireNonNull(sone);
-       }
-
-       @Nonnull
-       public Sone getSone() {
-               return sone;
-       }
-
-       @Override
-       public String getText() {
-               return SoneAccessor.getNiceName(sone);
-       }
-
-}
index 81ac751..39d26db 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.sone.text;
 
+import static com.google.common.base.Optional.absent;
+import static com.google.common.base.Optional.of;
 import static java.util.logging.Logger.getLogger;
 
 import java.io.BufferedReader;
@@ -24,6 +26,8 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Matcher;
@@ -39,8 +43,10 @@ import net.pterodactylus.sone.database.PostProvider;
 import net.pterodactylus.sone.database.SoneProvider;
 
 import com.google.common.base.Optional;
+import org.bitpedia.util.Base32;
 
 import freenet.keys.FreenetURI;
+import freenet.support.Base64;
 
 /**
  * {@link Parser} implementation that can recognize Freenet URIs.
@@ -55,6 +61,38 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
        /** Pattern to detect whitespace. */
        private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]");
 
+       private static class NextLink {
+
+               private final int position;
+               private final String link;
+               private final String remainder;
+               private final LinkType linkType;
+
+               private NextLink(int position, String link, String remainder, LinkType linkType) {
+                       this.position = position;
+                       this.link = link;
+                       this.remainder = remainder;
+                       this.linkType = linkType;
+               }
+
+               public int getPosition() {
+                       return position;
+               }
+
+               public String getLink() {
+                       return link;
+               }
+
+               public String getRemainder() {
+                       return remainder;
+               }
+
+               public LinkType getLinkType() {
+                       return linkType;
+               }
+
+       }
+
        /**
         * Enumeration for all recognized link types.
         *
@@ -69,7 +107,39 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                HTTP("http://", false),
                HTTPS("https://", false),
                SONE("sone://", false),
-               POST("post://", false);
+               POST("post://", false),
+
+               FREEMAIL("", true) {
+                       @Override
+                       public Optional<NextLink> findNext(String line) {
+                               int nextFreemailSuffix = line.indexOf(".freemail");
+                               if (nextFreemailSuffix < 54) {
+                                       /* 52 chars for the id, 1 on @, at least 1 for the local part. */
+                                       return absent();
+                               }
+                               if (line.charAt(nextFreemailSuffix - 53) != '@') {
+                                       return absent();
+                               }
+                               if (!line.substring(nextFreemailSuffix - 52, nextFreemailSuffix).matches("^[a-z2-7]*$")) {
+                                       return absent();
+                               }
+                               int startOfLocalPart = nextFreemailSuffix - 54;
+                               if (!isAllowedInLocalPart(line.charAt(startOfLocalPart))) {
+                                       return absent();
+                               }
+                               while ((startOfLocalPart > 0) && isAllowedInLocalPart(line.charAt(startOfLocalPart - 1))) {
+                                       startOfLocalPart--;
+                               }
+                               return of(new NextLink(startOfLocalPart, line.substring(startOfLocalPart, nextFreemailSuffix + 9), line.substring(nextFreemailSuffix + 9), this));
+                       }
+
+                       private boolean isAllowedInLocalPart(char character) {
+                               return ((character >= 'A') && (character <= 'Z'))
+                                               || ((character >= 'a') && (character <= 'z'))
+                                               || ((character >= '0') && (character <= '9'))
+                                               || (character == '.') || (character == '-') || (character == '_');
+                       }
+               };
 
                private final String scheme;
                private final boolean freenetLink;
@@ -92,6 +162,38 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                        return freenetLink;
                }
 
+               public Optional<NextLink> findNext(String line) {
+                       int nextLinkPosition = line.indexOf(getScheme());
+                       if (nextLinkPosition == -1) {
+                               return absent();
+                       }
+                       int endOfLink = findEndOfLink(line.substring(nextLinkPosition));
+                       return of(new NextLink(nextLinkPosition, line.substring(nextLinkPosition, nextLinkPosition + endOfLink), line.substring(nextLinkPosition + endOfLink), this));
+               }
+
+               private static int findEndOfLink(String line) {
+                       Matcher matcher = whitespacePattern.matcher(line);
+                       int endOfLink = matcher.find() ? matcher.start() : line.length();
+                       while (isPunctuation(line.charAt(endOfLink - 1))) {
+                               endOfLink--;
+                       }
+                       int openParens = 0;
+                       for (int i = 0; i < endOfLink; i++) {
+                               switch (line.charAt(i)) {
+                                       case '(':
+                                               openParens++;
+                                               break;
+                                       case ')':
+                                               openParens--;
+                                               if (openParens < 0) {
+                                                       return i;
+                                               }
+                                       default:
+                               }
+                       }
+                       return endOfLink;
+               }
+
        }
 
        /** The Sone provider. */
@@ -123,7 +225,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
        @Nonnull
        @Override
        public Iterable<Part> parse(@Nonnull String source, @Nullable SoneTextParserContext context) {
-               PartContainer parts = new PartContainer();
+               List<Part> parts = new ArrayList<>();
                try (Reader sourceReader = new StringReader(source);
                                BufferedReader bufferedReader = new BufferedReader(sourceReader)) {
                        String line;
@@ -147,7 +249,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                 */
                                boolean lineComplete = true;
                                while (line.length() > 0) {
-                                       Optional<NextLink> nextLink = NextLink.findNextLink(line);
+                                       Optional<NextLink> nextLink = findNextLink(line);
                                        if (!nextLink.isPresent()) {
                                                if (lineComplete && !lastLineEmpty) {
                                                        parts.add(new PlainTextPart("\n" + line));
@@ -175,8 +277,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                        }
                                        lineComplete = false;
 
-                                       int endOfLink = findEndOfLink(line);
-                                       String link = line.substring(0, endOfLink);
+                                       String link = nextLink.get().getLink();
                                        logger.log(Level.FINER, String.format("Found link: %s", link));
 
                                        /* if there is no text after the scheme, it’s not a link! */
@@ -203,9 +304,11 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                                case HTTPS:
                                                        renderHttpLink(parts, link, linkType);
                                                        break;
+                                               case FREEMAIL:
+                                                       renderFreemailLink(parts, link);
                                        }
 
-                                       line = line.substring(endOfLink);
+                                       line = nextLink.get().getRemainder();
                                }
                                lastLineEmpty = false;
                        }
@@ -214,16 +317,31 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                        throw new RuntimeException(ioe1);
                }
                for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
-                       Part part = parts.getPart(partIndex);
+                       Part part = parts.get(partIndex);
                        if (!(part instanceof PlainTextPart) || !"\n".equals(part.getText())) {
                                break;
                        }
-                       parts.removePart(partIndex);
+                       parts.remove(partIndex);
                }
                return parts;
        }
 
-       private void renderSoneLink(PartContainer parts, String line) {
+       public static Optional<NextLink> findNextLink(String line) {
+               int earliestLinkPosition = Integer.MAX_VALUE;
+               NextLink earliestNextLink = null;
+               for (LinkType possibleLinkType : LinkType.values()) {
+                       Optional<NextLink> nextLink = possibleLinkType.findNext(line);
+                       if (nextLink.isPresent()) {
+                               if (nextLink.get().getPosition() < earliestLinkPosition) {
+                                       earliestNextLink = nextLink.get();
+                                       earliestLinkPosition = earliestNextLink.getPosition();
+                               }
+                       }
+               }
+               return Optional.fromNullable(earliestNextLink);
+       }
+
+       private void renderSoneLink(List<Part> parts, String line) {
                if (line.length() >= (7 + 43)) {
                        String soneId = line.substring(7, 50);
                        Optional<Sone> sone = soneProvider.getSone(soneId);
@@ -233,7 +351,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                }
        }
 
-       private void renderPostLink(PartContainer parts, String line) {
+       private void renderPostLink(List<Part> parts, String line) {
                if (line.length() >= (7 + 36)) {
                        String postId = line.substring(7, 43);
                        Optional<Post> post = postProvider.getPost(postId);
@@ -247,10 +365,11 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                }
        }
 
-       private void renderFreenetLink(PartContainer parts, String link, LinkType linkType, @Nullable SoneTextParserContext context) {
+       private void renderFreenetLink(List<Part> parts, String link, LinkType linkType, @Nullable SoneTextParserContext context) {
                String name = link;
+               String linkWithoutParameters = link;
                if (name.indexOf('?') > -1) {
-                       name = name.substring(0, name.indexOf('?'));
+                       linkWithoutParameters = name = name.substring(0, name.indexOf('?'));
                }
                if (name.endsWith("/")) {
                        name = name.substring(0, name.length() - 1);
@@ -265,7 +384,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                name = link.substring(0, Math.min(9, link.length()));
                        }
                        boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
-                       parts.add(new FreenetLinkPart(link, name, fromPostingSone));
+                       parts.add(new FreenetLinkPart(link, name, linkWithoutParameters, fromPostingSone));
                } catch (MalformedURLException mue1) {
                        /* not a valid link, insert as plain text. */
                        parts.add(new PlainTextPart(link));
@@ -278,9 +397,8 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                }
        }
 
-       private void renderHttpLink(PartContainer parts, String link, LinkType linkType) {
-               String name;
-               name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
+       private void renderHttpLink(List<Part> parts, String link, LinkType linkType) {
+               String name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
                int firstSlash = name.indexOf('/');
                int lastSlash = name.lastIndexOf('/');
                if ((lastSlash - firstSlash) > 3) {
@@ -298,67 +416,16 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                parts.add(new LinkPart(link, name));
        }
 
-       private int findEndOfLink(String line) {
-               Matcher matcher = whitespacePattern.matcher(line);
-               int endOfLink = matcher.find() ? matcher.start() : line.length();
-               while ((endOfLink > 0) && isPunctuation(line.charAt(endOfLink - 1))) {
-                       endOfLink--;
-               }
-               int openParens = 0;
-               for (int i = 0; i < endOfLink; i++) {
-                       switch (line.charAt(i)) {
-                               case '(':
-                                       openParens++;
-                                       break;
-                               case ')':
-                                       openParens--;
-                                       if (openParens < 0) {
-                                               return i;
-                                       }
-                               default:
-                       }
-               }
-               return endOfLink;
+       private void renderFreemailLink(List<Part> parts, String line) {
+               int separator = line.indexOf('@');
+               String freemailId = line.substring(separator + 1, separator + 53);
+               String identityId = Base64.encode(Base32.decode(freemailId));
+               String emailLocalPart = line.substring(0, separator);
+               parts.add(new FreemailPart(emailLocalPart, freemailId, identityId));
        }
 
        private static boolean isPunctuation(char character) {
-               return (character == '.') || (character == ',');
-       }
-
-       private static class NextLink {
-
-               private final int position;
-               private final LinkType linkType;
-
-               private NextLink(int position, LinkType linkType) {
-                       this.position = position;
-                       this.linkType = linkType;
-               }
-
-               public int getPosition() {
-                       return position;
-               }
-
-               public LinkType getLinkType() {
-                       return linkType;
-               }
-
-               public static Optional<NextLink> findNextLink(String line) {
-                       int earliestLinkPosition = Integer.MAX_VALUE;
-                       LinkType linkType = null;
-                       for (LinkType possibleLinkType : LinkType.values()) {
-                               int nextLinkPosition = line.indexOf(possibleLinkType.getScheme());
-                               if (nextLinkPosition > -1) {
-                                       if (nextLinkPosition < earliestLinkPosition) {
-                                               earliestLinkPosition = nextLinkPosition;
-                                               linkType = possibleLinkType;
-                                       }
-                               }
-                       }
-                       return earliestLinkPosition < Integer.MAX_VALUE ?
-                                       Optional.of(new NextLink(earliestLinkPosition, linkType)) : Optional.<NextLink>absent();
-               }
-
+               return (character == '.') || (character == ',') || (character == '!') || (character == '?');
        }
 
 }
index 9e8afb4..ce4da2f 100644 (file)
@@ -13,7 +13,7 @@ import com.google.common.primitives.Longs;
  */
 public class NumberParsers {
 
-       @Nonnull
+       @Nullable
        public static Integer parseInt(@Nullable String text,
                        @Nullable Integer defaultValue) {
                if (text == null) {
@@ -23,7 +23,7 @@ public class NumberParsers {
                return (value == null) ? defaultValue : value;
        }
 
-       @Nonnull
+       @Nullable
        public static Long parseLong(@Nullable String text,
                        @Nullable Long defaultValue) {
                if (text == null) {
diff --git a/src/main/java/net/pterodactylus/sone/web/AboutPage.java b/src/main/java/net/pterodactylus/sone/web/AboutPage.java
deleted file mode 100644 (file)
index 87d9816..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Sone - AboutPage.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 net.pterodactylus.util.version.Version;
-
-/**
- * Shows some information about Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class AboutPage extends SoneTemplatePage {
-
-       private final String version;
-       private final int year;
-       private final String homepage;
-
-       public AboutPage(Template template, WebInterface webInterface, String version, int year, String homepage) {
-               super("about.html", template, "Page.About.Title", webInterface, false);
-               this.version = version;
-               this.year = year;
-               this.homepage = homepage;
-       }
-
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               templateContext.set("version", version);
-               templateContext.set("year", year);
-               templateContext.set("homepage", homepage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
deleted file mode 100644 (file)
index 7602230..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Sone - BookmarkPage.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.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 bookmark a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class BookmarkPage extends SoneTemplatePage {
-
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public BookmarkPage(Template template, WebInterface webInterface) {
-               super("bookmark.html", template, "Page.Bookmark.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().bookmarkPost(post.get());
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
deleted file mode 100644 (file)
index b273bdb..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Sone - BookmarksPage.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.parseInt;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-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;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-
-/**
- * Page that lets the user browse all his bookmarked posts.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class BookmarksPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new bookmarks page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public BookmarksPage(Template template, WebInterface webInterface) {
-               super("bookmarks.html", template, "Page.Bookmarks.Title", webInterface);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               Set<Post> allPosts = webInterface.getCore().getBookmarkedPosts();
-               Collection<Post> loadedPosts = Collections2.filter(allPosts, new Predicate<Post>() {
-
-                       @Override
-                       public boolean apply(Post post) {
-                               return post.isLoaded();
-                       }
-               });
-               List<Post> sortedPosts = new ArrayList<Post>(loadedPosts);
-               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());
-               templateContext.set("postsNotLoaded", allPosts.size() != loadedPosts.size());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
deleted file mode 100644 (file)
index 7423b67..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Sone - CreateAlbumPage.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.Album;
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty;
-import net.pterodactylus.sone.data.Sone;
-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 create a new album.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateAlbumPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “create album” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreateAlbumPage(Template template, WebInterface webInterface) {
-               super("createAlbum.html", template, "Page.CreateAlbum.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
-                       if (name.length() == 0) {
-                               templateContext.set("nameMissing", true);
-                               return;
-                       }
-                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 256).trim();
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
-                       Album parent = webInterface.getCore().getAlbum(parentId);
-                       if (parentId.equals("")) {
-                               parent = currentSone.getRootAlbum();
-                       }
-                       Album album = webInterface.getCore().createAlbum(currentSone, parent);
-                       try {
-                               album.modify().setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
-                       } catch (AlbumTitleMustNotBeEmpty atmnbe) {
-                               throw new RedirectException("emptyAlbumTitle.html");
-                       }
-                       webInterface.getCore().touchConfiguration();
-                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java b/src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
deleted file mode 100644 (file)
index ed478de..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Sone - CreatePostPage.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.Post;
-import net.pterodactylus.sone.data.Sone;
-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;
-
-/**
- * This page lets the user create a new {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreatePostPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “create post” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreatePostPage(Template template, WebInterface webInterface) {
-               super("createPost.html", template, "Page.CreatePost.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPATH METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               if (request.getMethod() == Method.POST) {
-                       String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
-                       if (text.length() != 0) {
-                               String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43);
-                               String recipientId = request.getHttpRequest().getPartAsStringFailsafe("recipient", 43);
-                               Sone currentSone = getCurrentSone(request.getToadletContext());
-                               Sone sender = webInterface.getCore().getLocalSone(senderId);
-                               if (sender == null) {
-                                       sender = currentSone;
-                               }
-                               Optional<Sone> recipient = webInterface.getCore().getSone(recipientId);
-                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
-                               webInterface.getCore().createPost(sender, recipient, text);
-                               throw new RedirectException(returnPage);
-                       }
-                       templateContext.set("errorTextEmpty", true);
-               }
-               templateContext.set("returnPage", returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
deleted file mode 100644 (file)
index 0bd5217..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Sone - CreateReplyPage.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.Post;
-import net.pterodactylus.sone.data.Sone;
-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;
-
-/**
- * This page lets the user post a reply to a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateReplyPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “create reply” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreateReplyPage(Template template, WebInterface webInterface) {
-               super("createReply.html", template, "Page.CreateReply.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
-               String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               if (request.getMethod() == Method.POST) {
-                       Optional<Post> post = webInterface.getCore().getPost(postId);
-                       if (!post.isPresent()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if (text.length() > 0) {
-                               String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43);
-                               Sone sender = webInterface.getCore().getLocalSone(senderId);
-                               if (sender == null) {
-                                       sender = getCurrentSone(request.getToadletContext());
-                               }
-                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
-                               webInterface.getCore().createReply(sender, post.get(), text);
-                               throw new RedirectException(returnPage);
-                       }
-                       templateContext.set("errorTextEmpty", true);
-               }
-               templateContext.set("postId", postId);
-               templateContext.set("text", text);
-               templateContext.set("returnPage", returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java b/src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
deleted file mode 100644 (file)
index a74204c..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Sone - CreateSonePage.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 java.util.logging.Logger.getLogger;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-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;
-
-/**
- * The “create Sone” page lets the user create a new Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateSonePage extends SoneTemplatePage {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(CreateSonePage.class.getName());
-
-       /**
-        * Creates a new “create Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreateSonePage(Template template, WebInterface webInterface) {
-               super("createSone.html", template, "Page.CreateSone.Title", webInterface, false);
-       }
-
-       //
-       // STATIC ACCESSORS
-       //
-
-       /**
-        * Returns a sorted list of all own identities that do not have the “Sone”
-        * context.
-        *
-        * @param core
-        *            The core
-        * @return The list of own identities without the “Sone” context
-        */
-       public static List<OwnIdentity> getOwnIdentitiesWithoutSone(Core core) {
-               List<OwnIdentity> identitiesWithoutSone = new ArrayList<OwnIdentity>();
-               Set<OwnIdentity> allOwnIdentity = core.getIdentityManager().getAllOwnIdentities();
-               for (OwnIdentity ownIdentity : allOwnIdentity) {
-                       if (!ownIdentity.hasContext("Sone")) {
-                               identitiesWithoutSone.add(ownIdentity);
-                       }
-               }
-               Collections.sort(identitiesWithoutSone, new Comparator<OwnIdentity>() {
-
-                       @Override
-                       public int compare(OwnIdentity leftIdentity, OwnIdentity rightIdentity) {
-                               return (leftIdentity.getNickname() + "@" + leftIdentity.getId()).compareToIgnoreCase(rightIdentity.getNickname() + "@" + rightIdentity.getId());
-                       }
-               });
-               return identitiesWithoutSone;
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
-               Collections.sort(localSones, Sone.NICE_NAME_COMPARATOR);
-               templateContext.set("sones", localSones);
-               List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
-               templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
-               if (request.getMethod() == Method.POST) {
-                       String id = request.getHttpRequest().getPartAsStringFailsafe("identity", 44);
-                       OwnIdentity selectedIdentity = null;
-                       for (OwnIdentity ownIdentity : ownIdentitiesWithoutSone) {
-                               if (ownIdentity.getId().equals(id)) {
-                                       selectedIdentity = ownIdentity;
-                                       break;
-                               }
-                       }
-                       if (selectedIdentity == null) {
-                               templateContext.set("errorNoIdentity", true);
-                               return;
-                       }
-                       /* create Sone. */
-                       Sone sone = webInterface.getCore().createSone(selectedIdentity);
-                       if (sone == null) {
-                               logger.log(Level.SEVERE, String.format("Could not create Sone for OwnIdentity: %s", selectedIdentity));
-                               /* TODO - go somewhere else */
-                       }
-
-                       /* log in the new Sone. */
-                       setCurrentSone(request.getToadletContext(), sone);
-                       throw new RedirectException("index.html");
-               }
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isEnabled(ToadletContext toadletContext) {
-               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
-                       return false;
-               }
-               return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java
deleted file mode 100644 (file)
index 581266d..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Sone - DeleteAlbumPage.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.Album;
-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 delete an {@link Album}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteAlbumPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “delete album” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteAlbumPage(Template template, WebInterface webInterface) {
-               super("deleteAlbum.html", template, "Page.DeleteAlbum.Title", webInterface, true);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
-                       Album album = webInterface.getCore().getAlbum(albumId);
-                       if (album == null) {
-                               throw new RedirectException("invalid.html");
-                       }
-                       if (!album.getSone().isLocal()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if (request.getHttpRequest().isPartSet("abortDelete")) {
-                               throw new RedirectException("imageBrowser.html?album=" + album.getId());
-                       }
-                       Album parentAlbum = album.getParent();
-                       webInterface.getCore().deleteAlbum(album);
-                       if (parentAlbum.equals(album.getSone().getRootAlbum())) {
-                               throw new RedirectException("imageBrowser.html?sone=" + album.getSone().getId());
-                       }
-                       throw new RedirectException("imageBrowser.html?album=" + parentAlbum.getId());
-               }
-               String albumId = request.getHttpRequest().getParam("album");
-               Album album = webInterface.getCore().getAlbum(albumId);
-               if (album == null) {
-                       throw new RedirectException("invalid.html");
-               }
-               templateContext.set("album", album);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java b/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java
deleted file mode 100644 (file)
index e1b2f37..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Sone - DeleteImagePage.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.Image;
-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 delete an {@link Image}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteImagePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “delete image” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteImagePage(Template template, WebInterface webInterface) {
-               super("deleteImage.html", template, "Page.DeleteImage.Title", webInterface, true);
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String imageId = (request.getMethod() == Method.POST) ? request.getHttpRequest().getPartAsStringFailsafe("image", 36) : request.getHttpRequest().getParam("image");
-               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 (request.getMethod() == Method.POST) {
-                       if (request.getHttpRequest().isPartSet("abortDelete")) {
-                               throw new RedirectException("imageBrowser.html?image=" + image.getId());
-                       }
-                       webInterface.getCore().deleteImage(image);
-                       throw new RedirectException("imageBrowser.html?album=" + image.getAlbum().getId());
-               }
-               templateContext.set("image", image);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java b/src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
deleted file mode 100644 (file)
index c2bae56..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Sone - DeletePostPage.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.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;
-
-/**
- * Lets the user delete a post they made.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeletePostPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “delete post” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeletePostPage(Template template, WebInterface webInterface) {
-               super("deletePost.html", template, "Page.DeletePost.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.GET) {
-                       String postId = request.getHttpRequest().getParam("post");
-                       String returnPage = request.getHttpRequest().getParam("returnPage");
-                       Optional<Post> post = webInterface.getCore().getPost(postId);
-                       if (!post.isPresent()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       templateContext.set("post", post.get());
-                       templateContext.set("returnPage", returnPage);
-               } else if (request.getMethod() == Method.POST) {
-                       String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
-                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-                       Optional<Post> post = webInterface.getCore().getPost(postId);
-                       if (!post.isPresent() || !post.get().getSone().isLocal()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if (request.getHttpRequest().isPartSet("confirmDelete")) {
-                               webInterface.getCore().deletePost(post.get());
-                               throw new RedirectException(returnPage);
-                       } else if (request.getHttpRequest().isPartSet("abortDelete")) {
-                               throw new RedirectException(returnPage);
-                       }
-                       templateContext.set("post", post);
-                       templateContext.set("returnPage", returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
deleted file mode 100644 (file)
index a137b82..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Sone - DeleteProfileFieldPage.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 confirm the deletion of a profile field.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteProfileFieldPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “delete profile field” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteProfileFieldPage(Template template, WebInterface webInterface) {
-               super("deleteProfileField.html", template, "Page.DeleteProfileField.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 POST request. */
-               if (request.getMethod() == Method.POST) {
-                       if (request.getHttpRequest().getPartAsStringFailsafe("confirm", 4).equals("true")) {
-                               fieldId = request.getHttpRequest().getParam("field");
-                               field = profile.getFieldById(fieldId);
-                               if (field == null) {
-                                       throw new RedirectException("invalid.html");
-                               }
-                               profile.removeField(field);
-                               currentSone.setProfile(profile);
-                       }
-                       throw new RedirectException("editProfile.html#profile-fields");
-               }
-
-               /* set current values in template. */
-               templateContext.set("field", field);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java
deleted file mode 100644 (file)
index 7112b73..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Sone - DeleteReplyPage.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.PostReply;
-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;
-
-/**
- * This page lets the user delete a reply.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteReplyPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “delete reply” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteReplyPage(Template template, WebInterface webInterface) {
-               super("deleteReply.html", template, "Page.DeleteReply.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
-               Optional<PostReply> reply = webInterface.getCore().getPostReply(replyId);
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               if (request.getMethod() == Method.POST) {
-                       if (!reply.isPresent() || !reply.get().getSone().isLocal()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if (request.getHttpRequest().isPartSet("confirmDelete")) {
-                               webInterface.getCore().deleteReply(reply.get());
-                               throw new RedirectException(returnPage);
-                       } else if (request.getHttpRequest().isPartSet("abortDelete")) {
-                               throw new RedirectException(returnPage);
-                       }
-               }
-               templateContext.set("reply", reply);
-               templateContext.set("returnPage", returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java b/src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
deleted file mode 100644 (file)
index 1390918..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Sone - DeleteSonePage.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;
-
-/**
- * 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.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteSonePage extends SoneTemplatePage {
-
-       /**
-        * Creates a new page that will delete a Sone.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteSonePage(Template template, WebInterface webInterface) {
-               super("deleteSone.html", template, "Page.DeleteSone.Title", webInterface, true);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       if (request.getHttpRequest().isPartSet("deleteSone")) {
-                               Sone currentSone = getCurrentSone(request.getToadletContext());
-                               webInterface.getCore().deleteSone(currentSone);
-                       }
-                       throw new RedirectException("index.html");
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java b/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
deleted file mode 100644 (file)
index c14a012..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - DismissNotificationPage.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.notify.Notification;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.base.Optional;
-
-/**
- * Page that lets the user dismiss a notification.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DismissNotificationPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “dismiss notifcation” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DismissNotificationPage(Template template, WebInterface webInterface) {
-               super("dismissNotification.html", template, "Page.DismissNotification.Title", webInterface);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
-               Optional<Notification> notification = webInterface.getNotification(notificationId);
-               if (notification.isPresent() && notification.get().isDismissable()) {
-                       notification.get().dismiss();
-               }
-               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
-               throw new RedirectException(returnPage);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/DistrustPage.java b/src/main/java/net/pterodactylus/sone/web/DistrustPage.java
deleted file mode 100644 (file)
index 50e3efb..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - DistrustPage.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 distrust another Sone. This will assign a
- * configurable (negative) amount of trust to an identity.
- *
- * @see Core#distrustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DistrustPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “distrust Sone” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DistrustPage(Template template, WebInterface webInterface) {
-               super("distrust.html", template, "Page.Distrust.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().distrustSone(currentSone, sone.get());
-                       }
-                       throw new RedirectException(returnPage);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
deleted file mode 100644 (file)
index 5829c8b..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Sone - EditAlbumPage.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.Album;
-import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty;
-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 the name and description of an album.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditAlbumPage extends SoneTemplatePage {
-
-       /**
-        * Creates a new “edit album” page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditAlbumPage(Template template, WebInterface webInterface) {
-               super("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               if (request.getMethod() == Method.POST) {
-                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
-                       Album album = webInterface.getCore().getAlbum(albumId);
-                       if (album == null) {
-                               throw new RedirectException("invalid.html");
-                       }
-                       if (!album.getSone().isLocal()) {
-                               throw new RedirectException("noPermission.html");
-                       }
-                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
-                               album.getParent().moveAlbumUp(album);
-                               webInterface.getCore().touchConfiguration();
-                               throw new RedirectException("imageBrowser.html?album=" + album.getParent().getId());
-                       } else if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
-                               album.getParent().moveAlbumDown(album);
-                               webInterface.getCore().touchConfiguration();
-                               throw new RedirectException("imageBrowser.html?album=" + album.getParent().getId());
-                       }
-                       String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
-                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
-                       try {
-                               album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
-                       } catch (AlbumTitleMustNotBeEmpty atmnbe) {
-                               throw new RedirectException("emptyAlbumTitle.html");
-                       }
-                       webInterface.getCore().touchConfiguration();
-                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
-               }
-       }
-
-}
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 e5bee78..0000000
+++ /dev/null
@@ -1,90 +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) || (existingField.equals(field))) {
-                               field.setName(name);
-                               currentSone.setProfile(profile);
-                               throw new RedirectException("editProfile.html#profile-fields");
-                       }
-                       templateContext.set("duplicateFieldName", true);
-               }
-
-               /* 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 acc6354..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 = getCurrentSone(request.getToadletContext(), false);
-               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 da93a02..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 = getCurrentSone(request.getToadletContext(), false);
-               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/LoginPage.java b/src/main/java/net/pterodactylus/sone/web/LoginPage.java
deleted file mode 100644 (file)
index f9bd371..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Sone - LoginPage.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 java.util.logging.Logger.getLogger;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-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;
-
-/**
- * The login page manages logging the user in.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LoginPage extends SoneTemplatePage {
-
-       /** The logger. */
-       @SuppressWarnings("unused")
-       private static final Logger logger = getLogger(LoginPage.class.getName());
-
-       /**
-        * Creates a new login page.
-        *
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LoginPage(Template template, WebInterface webInterface) {
-               super("login.html", template, "Page.Login.Title", webInterface, false);
-       }
-
-       //
-       // TEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-               /* get all own identities. */
-               List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
-               Collections.sort(localSones, Sone.NICE_NAME_COMPARATOR);
-               templateContext.set("sones", localSones);
-               if (request.getMethod() == Method.POST) {
-                       String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone-id", 100);
-                       Sone selectedSone = webInterface.getCore().getLocalSone(soneId);
-                       if (selectedSone != null) {
-                               setCurrentSone(request.getToadletContext(), selectedSone);
-                               String target = request.getHttpRequest().getParam("target");
-                               if ((target == null) || (target.length() == 0)) {
-                                       target = "index.html";
-                               }
-                               throw new RedirectException(target);
-                       }
-               }
-               List<OwnIdentity> ownIdentitiesWithoutSone = CreateSonePage.getOwnIdentitiesWithoutSone(webInterface.getCore());
-               templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getRedirectTarget(FreenetRequest request) {
-               if (getCurrentSone(request.getToadletContext(), false) != null) {
-                       return "index.html";
-               }
-               return null;
-       }
-
-       //
-       // SONETEMPLATEPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isEnabled(ToadletContext toadletContext) {
-               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
-                       return false;
-               }
-               return getCurrentSone(toadletContext, false) == null;
-       }
-
-}
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 f7a254c..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 (getCurrentSone(toadletContext, false) != 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 dc1841b..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 if (type.equals("sone")) {
-                               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 406054d..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(getCurrentSone(request.getToadletContext(), false)));
-               for (PostReply reply : webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false))) {
-                       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<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/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java
deleted file mode 100644 (file)
index 2bd1bfd..0000000
+++ /dev/null
@@ -1,162 +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.Sone.ShowCustomAvatars;
-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.getCurrentSone(request.getToadletContext(), false);
-               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(ShowCustomAvatars.valueOf(showCustomAvatars));
-                               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("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 f89bbed..0000000
+++ /dev/null
@@ -1,91 +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.Closer;
-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);
-               InputStream fileInputStream;
-               try {
-                       fileInputStream = new FileInputStream(new File(filesystemPath, filename));
-               } catch (FileNotFoundException fnfe1) {
-                       return response.setStatusCode(404).setStatusText("Not found.");
-               }
-               OutputStream contentOutputStream = response.getContent();
-               try {
-                       StreamCopier.copy(fileInputStream, contentOutputStream);
-               } finally {
-                       Closer.close(fileInputStream);
-                       Closer.close(contentOutputStream);
-               }
-               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 78a2b9e..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 = getCurrentSone(request.getToadletContext(), false);
-               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 bf9614d..0000000
+++ /dev/null
@@ -1,313 +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 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 current session, creating a new session if there is no
-        * current session.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       protected Session getCurrentSession(ToadletContext toadletContenxt) {
-               return webInterface.getCurrentSession(toadletContenxt);
-       }
-
-       /**
-        * Returns the current session, creating a new session if there is no
-        * current session and {@code create} is {@code true}.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if there is no current
-        *            session, {@code false} otherwise
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       protected Session getCurrentSession(ToadletContext toadletContenxt, boolean create) {
-               return webInterface.getCurrentSession(toadletContenxt, create);
-       }
-
-       /**
-        * 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.getCurrentSone(toadletContext);
-       }
-
-       /**
-        * Returns the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if no session exists,
-        *            {@code false} to not create a new session
-        * @return The currently logged in Sone, or {@code null} if no Sone is
-        *         currently logged in
-        */
-       protected Sone getCurrentSone(ToadletContext toadletContext, boolean create) {
-               return webInterface.getCurrentSone(toadletContext, create);
-       }
-
-       /**
-        * 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 = getCurrentSone(request.getToadletContext(), false);
-               templateContext.set("core", webInterface.getCore());
-               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(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected String getRedirectTarget(FreenetRequest request) {
-               if (requiresLogin() && (getCurrentSone(request.getToadletContext(), false) == 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("%26");
-                                       }
-                                       String[] parameterValues = httpRequest.getMultipleParam(parameterName);
-                                       for (String parameterValue : parameterValues) {
-                                               try {
-                                                       requestParameters.append(URLEncoder.encode(parameterName, "UTF-8")).append("%3d").append(URLEncoder.encode(parameterValue, "UTF-8"));
-                                               } catch (UnsupportedEncodingException uee1) {
-                                                       /* A JVM without UTF-8? I don’t think so. */
-                                               }
-                                       }
-                               }
-                               originalUrl += "?" + requestParameters.toString();
-                       }
-                       return "login.html?target=" + originalUrl;
-               }
-               return null;
-       }
-
-       /**
-        * {@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 getCurrentSone(toadletContext, false) != 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 8edf2da..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 b7e3287..6401b43 100644 (file)
@@ -30,6 +30,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TimeZone;
 import java.util.UUID;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -40,6 +41,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.core.ElementLoader;
 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
@@ -72,6 +74,9 @@ import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.main.Loaders;
 import net.pterodactylus.sone.main.ReparseFilter;
 import net.pterodactylus.sone.main.SonePlugin;
+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.notify.ListNotification;
 import net.pterodactylus.sone.notify.ListNotificationFilter;
 import net.pterodactylus.sone.notify.PostVisibilityFilter;
@@ -84,12 +89,16 @@ import net.pterodactylus.sone.template.IdentityAccessor;
 import net.pterodactylus.sone.template.ImageAccessor;
 import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
+import net.pterodactylus.sone.template.LinkedElementRenderFilter;
+import net.pterodactylus.sone.template.LinkedElementsFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ProfileAccessor;
+import net.pterodactylus.sone.template.RenderFilter;
 import net.pterodactylus.sone.template.ReplyAccessor;
 import net.pterodactylus.sone.template.ReplyGroupFilter;
 import net.pterodactylus.sone.template.RequestChangeFilter;
+import net.pterodactylus.sone.template.ShortenFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.template.TrustAccessor;
@@ -98,6 +107,7 @@ import net.pterodactylus.sone.template.UnknownDateFilter;
 import net.pterodactylus.sone.text.Part;
 import net.pterodactylus.sone.text.SonePart;
 import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.text.TimeTextConverter;
 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
@@ -111,12 +121,13 @@ import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
-import net.pterodactylus.sone.web.ajax.GetTranslationPage;
+import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
@@ -130,6 +141,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.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;
@@ -170,7 +224,7 @@ import com.google.inject.Inject;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class WebInterface {
+public class WebInterface implements SessionProvider {
 
        /** The logger. */
        private static final Logger logger = getLogger(WebInterface.class.getName());
@@ -198,11 +252,18 @@ public class WebInterface {
 
        /** The parser filter. */
        private final ParserFilter parserFilter;
+       private final ShortenFilter shortenFilter;
+       private final RenderFilter renderFilter;
 
        private final ListNotificationFilter listNotificationFilter;
        private final PostVisibilityFilter postVisibilityFilter;
        private final ReplyVisibilityFilter replyVisibilityFilter;
 
+       private final ElementLoader elementLoader;
+       private final LinkedElementRenderFilter linkedElementRenderFilter;
+       private final TimeTextConverter timeTextConverter = new TimeTextConverter();
+       private final L10nFilter l10nFilter = new L10nFilter(this);
+
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
 
@@ -252,19 +313,20 @@ public class WebInterface {
         *            The Sone plugin
         */
        @Inject
-       public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter) {
+       public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ElementLoader elementLoader) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
                this.postVisibilityFilter = postVisibilityFilter;
                this.replyVisibilityFilter = replyVisibilityFilter;
+               this.elementLoader = elementLoader;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                soneTextParser = new SoneTextParser(getCore(), getCore());
 
                templateContextFactory = new TemplateContextFactory();
                templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
                templateContextFactory.addAccessor(Collection.class, new CollectionAccessor());
-               templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore()));
+               templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore(), new TimeTextConverter()));
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
                templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
@@ -284,7 +346,11 @@ public class WebInterface {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), templateContextFactory, soneTextParser));
+               templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), soneTextParser));
+               templateContextFactory.addFilter("shorten", shortenFilter = new ShortenFilter());
+               templateContextFactory.addFilter("render", renderFilter = new RenderFilter(getCore(), templateContextFactory));
+               templateContextFactory.addFilter("linked-elements", new LinkedElementsFilter(elementLoader));
+               templateContextFactory.addFilter("render-linked-element", linkedElementRenderFilter = new LinkedElementRenderFilter(templateContextFactory));
                templateContextFactory.addFilter("reparse", new ReparseFilter());
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
@@ -344,6 +410,7 @@ public class WebInterface {
         *
         * @return The Sone core
         */
+       @Nonnull
        public Core getCore() {
                return sonePlugin.core();
        }
@@ -357,68 +424,32 @@ public class WebInterface {
                return templateContextFactory;
        }
 
-       /**
-        * Returns the current session, creating a new session if there is no
-        * current session.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       public Session getCurrentSession(ToadletContext toadletContenxt) {
-               return getCurrentSession(toadletContenxt, true);
+       private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
+               return getSessionManager().useSession(toadletContenxt);
        }
 
-       /**
-        * Returns the current session, creating a new session if there is no
-        * current session and {@code create} is {@code true}.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if there is no current
-        *            session, {@code false} otherwise
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       public Session getCurrentSession(ToadletContext toadletContenxt, boolean create) {
-               Session session = getSessionManager().useSession(toadletContenxt);
-               if (create && (session == null)) {
+       private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
+               Session session = getCurrentSessionWithoutCreation(toadletContenxt);
+               if (session == null) {
                        session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
                }
                return session;
        }
 
-       /**
-        * Returns the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @return The currently logged in Sone, or {@code null} if no Sone is
-        *         currently logged in
-        */
-       public Sone getCurrentSone(ToadletContext toadletContext) {
-               return getCurrentSone(toadletContext, true);
+       public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
+               Collection<Sone> localSones = getCore().getLocalSones();
+               if (localSones.size() == 1) {
+                       return localSones.iterator().next();
+               }
+               return getCurrentSone(getOrCreateCurrentSession(toadletContext));
        }
 
-       /**
-        * Returns the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if no session exists,
-        *            {@code false} to not create a new session
-        * @return The currently logged in Sone, or {@code null} if no Sone is
-        *         currently logged in
-        */
-       public Sone getCurrentSone(ToadletContext toadletContext, boolean create) {
+       public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
                Collection<Sone> localSones = getCore().getLocalSones();
                if (localSones.size() == 1) {
                        return localSones.iterator().next();
                }
-               return getCurrentSone(getCurrentSession(toadletContext, create));
+               return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
        }
 
        /**
@@ -429,7 +460,7 @@ public class WebInterface {
         * @return The currently logged in Sone, or {@code null} if no Sone is
         *         currently logged in
         */
-       public Sone getCurrentSone(Session session) {
+       private Sone getCurrentSone(Session session) {
                if (session == null) {
                        return null;
                }
@@ -440,6 +471,12 @@ public class WebInterface {
                return getCore().getLocalSone(soneId);
        }
 
+       @Override
+       @Nullable
+       public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
+               return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
+       }
+
        /**
         * Sets the currently logged in Sone.
         *
@@ -448,8 +485,9 @@ public class WebInterface {
         * @param sone
         *            The Sone to set as currently logged in
         */
-       public void setCurrentSone(ToadletContext toadletContext, Sone sone) {
-               Session session = getCurrentSession(toadletContext);
+       @Override
+       public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
+               Session session = getOrCreateCurrentSession(toadletContext);
                if (sone == null) {
                        session.removeAttribute("Sone.CurrentSone");
                } else {
@@ -717,7 +755,7 @@ public class WebInterface {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue"));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.getPluginVersion(), SonePlugin.getYear(), SonePlugin.getHomepage()), "About"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(SonePlugin.getYear()), new PluginHomepage(SonePlugin.getHomepage())), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("emptyImageTitle.html", emptyImageTitleTemplate, "Page.EmptyImageTitle.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("emptyAlbumTitle.html", emptyAlbumTitleTemplate, "Page.EmptyAlbumTitle.Title", this)));
@@ -728,15 +766,16 @@ public class WebInterface {
                pageToadlets.add(pageToadletFactory.createPageToadlet(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault())));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationsAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetReplyAjaxPage(this, replyTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetPostAjaxPage(this, postTemplate)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTimesAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault())));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyAjaxPage(this)));
@@ -745,7 +784,7 @@ public class WebInterface {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumAjaxPage(this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this, parserFilter)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
deleted file mode 100644 (file)
index ce5276b..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - BookmarkAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-import com.google.common.base.Optional;
-
-/**
- * AJAX page that lets the user bookmark a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class BookmarkAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new bookmark AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public BookmarkAjaxPage(WebInterface webInterface) {
-               super("bookmark.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String id = request.getHttpRequest().getParam("post", null);
-               if ((id == null) || (id.length() == 0)) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               Optional<Post> post = webInterface.getCore().getPost(id);
-               if (post.isPresent()) {
-                       webInterface.getCore().bookmarkPost(post.get());
-               }
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
deleted file mode 100644 (file)
index bf82328..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Sone - CreatePostAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-import com.google.common.base.Optional;
-
-/**
- * AJAX handler that creates a new post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreatePostAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “create post” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreatePostAjaxPage(WebInterface webInterface) {
-               super("createPost.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone sone = getCurrentSone(request.getToadletContext());
-               if (sone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               String recipientId = request.getHttpRequest().getParam("recipient");
-               Optional<Sone> recipient = webInterface.getCore().getSone(recipientId);
-               String senderId = request.getHttpRequest().getParam("sender");
-               Sone sender = webInterface.getCore().getLocalSone(senderId);
-               if (sender == null) {
-                       sender = sone;
-               }
-               String text = request.getHttpRequest().getParam("text");
-               if ((text == null) || (text.trim().length() == 0)) {
-                       return createErrorJsonObject("text-required");
-               }
-               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
-               Post newPost = webInterface.getCore().createPost(sender, recipient, text);
-               return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", newPost.getRecipientId().orNull());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
deleted file mode 100644 (file)
index fe2ae85..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - CreateReplyAjaxPage.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.ajax;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * This AJAX page create a reply.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateReplyAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “create reply” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public CreateReplyAjaxPage(WebInterface webInterface) {
-               super("createReply.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String postId = request.getHttpRequest().getParam("post");
-               String text = request.getHttpRequest().getParam("text").trim();
-               String senderId = request.getHttpRequest().getParam("sender");
-               Sone sender = webInterface.getCore().getLocalSone(senderId);
-               if (sender == null) {
-                       sender = getCurrentSone(request.getToadletContext());
-               }
-               Optional<Post> post = webInterface.getCore().getPost(postId);
-               if (!post.isPresent()) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
-               PostReply reply = webInterface.getCore().createReply(sender, post.get(), text);
-               return createSuccessJsonObject().put("reply", reply.getId()).put("sone", sender.getId());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java
deleted file mode 100644 (file)
index cfc1415..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - DeletePostAjaxPage.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.ajax;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * This AJAX page deletes a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeletePostAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new AJAX page that deletes a post.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeletePostAjaxPage(WebInterface webInterface) {
-               super("deletePost.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String postId = request.getHttpRequest().getParam("post");
-               Optional<Post> post = webInterface.getCore().getPost(postId);
-               if (!post.isPresent()) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               if (!post.get().getSone().isLocal()) {
-                       return createErrorJsonObject("not-authorized");
-               }
-               webInterface.getCore().deletePost(post.get());
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
deleted file mode 100644 (file)
index 7d937b5..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Sone - DeleteProfileFieldAjaxPage.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.ajax;
-
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-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 com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
-
-/**
- * AJAX page that lets the user delete a profile field.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteProfileFieldAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “delete profile field” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteProfileFieldAjaxPage(WebInterface webInterface) {
-               super("deleteProfileField.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String fieldId = request.getHttpRequest().getParam("field");
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               Profile profile = currentSone.getProfile();
-               Field field = profile.getFieldById(fieldId);
-               if (field == null) {
-                       return createErrorJsonObject("invalid-field-id");
-               }
-               profile.removeField(field);
-               currentSone.setProfile(profile);
-               webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject().put("field", new ObjectNode(instance).put("id", new TextNode(field.getId())));
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java
deleted file mode 100644 (file)
index 7b2c3c9..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - DeleteReplyAjaxPage.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.ajax;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * This AJAX page deletes a reply.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteReplyAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new AJAX page that deletes a reply.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DeleteReplyAjaxPage(WebInterface webInterface) {
-               super("deleteReply.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String replyId = request.getHttpRequest().getParam("reply");
-               Optional<PostReply> reply = webInterface.getCore().getPostReply(replyId);
-               if (!reply.isPresent()) {
-                       return createErrorJsonObject("invalid-reply-id");
-               }
-               if (!reply.get().getSone().isLocal()) {
-                       return createErrorJsonObject("not-authorized");
-               }
-               webInterface.getCore().deleteReply(reply.get());
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
deleted file mode 100644 (file)
index 4661851..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Sone - DismissNotificationAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.notify.Notification;
-
-import com.google.common.base.Optional;
-
-/**
- * AJAX page that lets the user dismiss a notification.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DismissNotificationAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “dismiss notification” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DismissNotificationAjaxPage(WebInterface webInterface) {
-               super("dismissNotification.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String notificationId = request.getHttpRequest().getParam("notification");
-               Optional<Notification> notification = webInterface.getNotification(notificationId);
-               if (!notification.isPresent()) {
-                       return createErrorJsonObject("invalid-notification-id");
-               }
-               if (!notification.get().isDismissable()) {
-                       return createErrorJsonObject("not-dismissable");
-               }
-               notification.get().dismiss();
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java
deleted file mode 100644 (file)
index 26a1207..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - DistrustAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user distrust a Sone.
- *
- * @see Core#distrustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DistrustAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “distrust Sone” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public DistrustAjaxPage(WebInterface webInterface) {
-               super("distrustSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               if (!sone.isPresent()) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               webInterface.getCore().distrustSone(currentSone, sone.get());
-               return createSuccessJsonObject().put("trustValue", webInterface.getCore().getPreferences().getNegativeTrust());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java
deleted file mode 100644 (file)
index 9817fd8..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Sone - EditAlbumAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * Page that stores a user’s album modifications.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditAlbumAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new edit album AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditAlbumAjaxPage(WebInterface webInterface) {
-               super("editAlbum.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String albumId = request.getHttpRequest().getParam("album");
-               Album album = webInterface.getCore().getAlbum(albumId);
-               if (album == null) {
-                       return createErrorJsonObject("invalid-album-id");
-               }
-               if (!album.getSone().isLocal()) {
-                       return createErrorJsonObject("not-authorized");
-               }
-               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
-                       Album swappedAlbum = album.getParent().moveAlbumUp(album);
-                       webInterface.getCore().touchConfiguration();
-                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
-               }
-               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
-                       Album swappedAlbum = album.getParent().moveAlbumDown(album);
-                       webInterface.getCore().touchConfiguration();
-                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
-               }
-               String title = request.getHttpRequest().getParam("title").trim();
-               String description = request.getHttpRequest().getParam("description").trim();
-               try {
-                       album.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
-                       webInterface.getCore().touchConfiguration();
-                       return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription());
-               } catch (IllegalStateException e) {
-                       return createErrorJsonObject("invalid-album-title");
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
deleted file mode 100644 (file)
index 6f67ccb..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Sone - EditImageAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.template.ParserFilter;
-import net.pterodactylus.sone.text.TextFilter;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.collect.ImmutableMap;
-
-/**
- * Page that stores a user’s image modifications.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditImageAjaxPage extends JsonPage {
-
-       /** Parser for image descriptions. */
-       private final ParserFilter parserFilter;
-
-       /**
-        * Creates a new edit image AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        * @param parserFilter
-        *            The parser filter for image descriptions
-        */
-       public EditImageAjaxPage(WebInterface webInterface, ParserFilter parserFilter) {
-               super("editImage.ajax", webInterface);
-               this.parserFilter = parserFilter;
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String imageId = request.getHttpRequest().getParam("image");
-               Image image = webInterface.getCore().getImage(imageId, false);
-               if (image == null) {
-                       return createErrorJsonObject("invalid-image-id");
-               }
-               if (!image.getSone().isLocal()) {
-                       return createErrorJsonObject("not-authorized");
-               }
-               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
-                       Image swappedImage = image.getAlbum().moveImageUp(image);
-                       webInterface.getCore().touchConfiguration();
-                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
-               }
-               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
-                       Image swappedImage = image.getAlbum().moveImageDown(image);
-                       webInterface.getCore().touchConfiguration();
-                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
-               }
-               String title = request.getHttpRequest().getParam("title").trim();
-               if (title.isEmpty()) {
-                       return createErrorJsonObject("invalid-image-title");
-               }
-               String description = request.getHttpRequest().getParam("description").trim();
-               image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
-               webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription()).put("parsedDescription", (String) parserFilter.format(new TemplateContext(), image.getDescription(), ImmutableMap.<String, Object>builder().put("sone", image.getSone()).build()));
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
deleted file mode 100644 (file)
index b81cf60..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Sone - EditProfileFieldAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user rename a profile field.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class EditProfileFieldAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “edit profile field” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public EditProfileFieldAjaxPage(WebInterface webInterface) {
-               super("editProfileField.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String fieldId = request.getHttpRequest().getParam("field");
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               Profile profile = currentSone.getProfile();
-               Field field = profile.getFieldById(fieldId);
-               if (field == null) {
-                       return createErrorJsonObject("invalid-field-id");
-               }
-               String name = request.getHttpRequest().getParam("name", "").trim();
-               if (name.length() == 0) {
-                       return createErrorJsonObject("invalid-parameter-name");
-               }
-               Field existingField = profile.getFieldByName(name);
-               if ((existingField != null) && !existingField.equals(field)) {
-                       return createErrorJsonObject("duplicate-field-name");
-               }
-               field.setName(name);
-               currentSone.setProfile(profile);
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java
deleted file mode 100644 (file)
index 6a2d8ce..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Sone - FollowSoneAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets a Sone follow another Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FollowSoneAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “follow Sone” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public FollowSoneAjaxPage(WebInterface webInterface) {
-               super("followSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               if (!sone.isPresent()) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               webInterface.getCore().followSone(currentSone, soneId);
-               webInterface.getCore().markSoneKnown(sone.get());
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java
deleted file mode 100644 (file)
index 7bb75e6..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Sone - GetLikesAjaxPage.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.ajax;
-
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-import static net.pterodactylus.sone.data.Sone.NICE_NAME_COMPARATOR;
-
-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 com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.base.Optional;
-import com.google.common.collect.FluentIterable;
-
-/**
- * AJAX page that retrieves the number of “likes” a {@link Post} has.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetLikesAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “get post likes” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetLikesAjaxPage(WebInterface webInterface) {
-               super("getLikes.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String type = request.getHttpRequest().getParam("type", null);
-               String id = request.getHttpRequest().getParam(type, null);
-               if ((id == null) || (id.length() == 0)) {
-                       return createErrorJsonObject("invalid-" + type + "-id");
-               }
-               if ("post".equals(type)) {
-                       Optional<Post> post = webInterface.getCore().getPost(id);
-                       if (!post.isPresent()) {
-                               return createErrorJsonObject("invalid-post-id");
-                       }
-                       Set<Sone> sones = webInterface.getCore().getLikes(post.get());
-                       return createSuccessJsonObject().put("likes", sones.size()).put("sones", getSones(sones));
-               } else if ("reply".equals(type)) {
-                       Optional<PostReply> reply = webInterface.getCore().getPostReply(id);
-                       if (!reply.isPresent()) {
-                               return createErrorJsonObject("invalid-reply-id");
-                       }
-                       Set<Sone> sones = webInterface.getCore().getLikes(reply.get());
-                       return createSuccessJsonObject().put("likes", sones.size()).put("sones", getSones(sones));
-               }
-               return createErrorJsonObject("invalid-type");
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a JSON array (containing the IDs and the nice names) from the
-        * given Sones, after sorting them by name.
-        *
-        * @param sones
-        *            The Sones to convert to an array
-        * @return The Sones, sorted by name
-        */
-       private static JsonNode getSones(Set<Sone> sones) {
-               ArrayNode soneArray = new ArrayNode(instance);
-               for (Sone sone : FluentIterable.from(sones).toSortedList(NICE_NAME_COMPARATOR)) {
-                       soneArray.add(new ObjectNode(instance).put("id", sone.getId()).put("name", SoneAccessor.getNiceName(sone)));
-               }
-               return soneArray;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java
deleted file mode 100644 (file)
index 65d3943..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Sone - GetNotificationsAjaxPage.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.ajax;
-
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-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.util.notify.Notification;
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-/**
- * AJAX handler to return all current notifications.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetNotificationsAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “get notifications” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetNotificationsAjaxPage(WebInterface webInterface) {
-               super("getNotifications.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
-               Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
-               ArrayNode jsonNotifications = new ArrayNode(instance);
-               for (Notification notification : notifications) {
-                       jsonNotifications.add(createJsonNotification(request, notification));
-               }
-               return createSuccessJsonObject().put("notificationHash", notifications.hashCode()).put("notifications", jsonNotifications).put("options", createJsonOptions(currentSone));
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a JSON object from the given notification.
-        *
-        * @param request
-        *            The request to load the session from
-        * @param notification
-        *            The notification to create a JSON object
-        * @return The JSON object
-        */
-       private JsonNode createJsonNotification(FreenetRequest request, Notification notification) {
-               ObjectNode jsonNotification = new ObjectNode(instance);
-               jsonNotification.put("id", notification.getId());
-               StringWriter notificationWriter = new StringWriter();
-               try {
-                       if (notification instanceof TemplateNotification) {
-                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
-                               templateContext.set("core", webInterface.getCore());
-                               templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
-                               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());
-                               templateContext.set("notification", notification);
-                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
-                       } else {
-                               notification.render(notificationWriter);
-                       }
-               } catch (IOException ioe1) {
-                       /* StringWriter never throws, ignore. */
-               }
-               jsonNotification.put("text", notificationWriter.toString());
-               jsonNotification.put("createdTime", notification.getCreatedTime());
-               jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
-               jsonNotification.put("dismissable", notification.isDismissable());
-               return jsonNotification;
-       }
-
-       /**
-        * Creates a JSON object that contains all options that are currently in
-        * effect for the given Sone (or overall, if the given Sone is {@code null}
-        * ).
-        *
-        * @param currentSone
-        *            The current Sone (may be {@code null})
-        * @return The current options
-        */
-       private static JsonNode createJsonOptions(Sone currentSone) {
-               ObjectNode options = new ObjectNode(instance);
-               if (currentSone != null) {
-                       options.put("ShowNotification/NewSones", currentSone.getOptions().isShowNewSoneNotifications());
-                       options.put("ShowNotification/NewPosts", currentSone.getOptions().isShowNewPostNotifications());
-                       options.put("ShowNotification/NewReplies", currentSone.getOptions().isShowNewReplyNotifications());
-               }
-               return options;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
deleted file mode 100644 (file)
index 60e94ce..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Sone - GetPostAjaxPage.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.ajax;
-
-import java.io.StringWriter;
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-import com.google.common.base.Optional;
-
-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.io.Closer;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateException;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-/**
- * This AJAX handler retrieves information and rendered representation of a
- * {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetPostAjaxPage extends JsonPage {
-
-       /** The template to render for posts. */
-       private final Template postTemplate;
-
-       /**
-        * Creates a new “get post” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        * @param postTemplate
-        *            The template to render for posts
-        */
-       public GetPostAjaxPage(WebInterface webInterface, Template postTemplate) {
-               super("getPost.ajax", webInterface);
-               this.postTemplate = postTemplate;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String postId = request.getHttpRequest().getParam("post");
-               Optional<Post> post = webInterface.getCore().getPost(postId);
-               if (!post.isPresent()) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               return createSuccessJsonObject().put("post", createJsonPost(request, post.get(), getCurrentSone(request.getToadletContext())));
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a JSON object from the given post. The JSON object will only
-        * contain the ID of the post, its time, and its rendered HTML code.
-        *
-        * @param request
-        *            The request being processed
-        * @param post
-        *            The post to create a JSON object from
-        * @param currentSone
-        *            The currently logged in Sone (to store in the template)
-        * @return The JSON representation of the post
-        */
-       private JsonNode createJsonPost(FreenetRequest request, Post post, Sone currentSone) {
-               ObjectNode jsonPost = new ObjectNode(instance);
-               jsonPost.put("id", post.getId());
-               jsonPost.put("sone", post.getSone().getId());
-               jsonPost.put("recipient", post.getRecipientId().orNull());
-               jsonPost.put("time", post.getTime());
-               StringWriter stringWriter = new StringWriter();
-               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
-               templateContext.set("core", webInterface.getCore());
-               templateContext.set("request", request);
-               templateContext.set("post", post);
-               templateContext.set("currentSone", currentSone);
-               templateContext.set("localSones", webInterface.getCore().getLocalSones());
-               try {
-                       postTemplate.render(templateContext, stringWriter);
-               } catch (TemplateException te1) {
-                       /* TODO - shouldn’t happen. */
-               } finally {
-                       Closer.close(stringWriter);
-               }
-               jsonPost.put("html", stringWriter.toString());
-               return jsonPost;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
deleted file mode 100644 (file)
index c66f47a..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Sone - GetReplyAjaxPage.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.ajax;
-
-import java.io.StringWriter;
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-import com.google.common.base.Optional;
-
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-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.template.TemplateException;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-/**
- * This AJAX page returns the details of a reply.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetReplyAjaxPage extends JsonPage {
-
-       /** The template to render. */
-       private final Template replyTemplate;
-
-       /**
-        * Creates a new “get reply” page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        * @param replyTemplate
-        *            The template to render
-        */
-       public GetReplyAjaxPage(WebInterface webInterface, Template replyTemplate) {
-               super("getReply.ajax", webInterface);
-               this.replyTemplate = replyTemplate;
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String replyId = request.getHttpRequest().getParam("reply");
-               Optional<PostReply> reply = webInterface.getCore().getPostReply(replyId);
-               if (!reply.isPresent()) {
-                       return createErrorJsonObject("invalid-reply-id");
-               }
-               return createSuccessJsonObject().put("reply", createJsonReply(request, reply.get(), getCurrentSone(request.getToadletContext())));
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a JSON representation of the given reply.
-        *
-        * @param request
-        *            The request being processed
-        * @param reply
-        *            The reply to convert
-        * @param currentSone
-        *            The currently logged in Sone (to store in the template)
-        * @return The JSON representation of the reply
-        */
-       private JsonNode createJsonReply(FreenetRequest request, PostReply reply, Sone currentSone) {
-               ObjectNode jsonReply = new ObjectNode(instance);
-               jsonReply.put("id", reply.getId());
-               jsonReply.put("postId", reply.getPostId());
-               jsonReply.put("soneId", reply.getSone().getId());
-               jsonReply.put("time", reply.getTime());
-               StringWriter stringWriter = new StringWriter();
-               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
-               templateContext.set("core", webInterface.getCore());
-               templateContext.set("request", request);
-               templateContext.set("reply", reply);
-               templateContext.set("currentSone", currentSone);
-               try {
-                       replyTemplate.render(templateContext, stringWriter);
-               } catch (TemplateException te1) {
-                       /* TODO - shouldn’t happen. */
-               } finally {
-                       Closer.close(stringWriter);
-               }
-               return jsonReply.put("html", stringWriter.toString());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
deleted file mode 100644 (file)
index 2ca7c23..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Sone - GetStatusAjaxPage.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.ajax;
-
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-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.notify.ReplyVisibilityFilter;
-import net.pterodactylus.sone.template.SoneAccessor;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.notify.Notification;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-/**
- * The “get status” AJAX handler returns all information that is necessary to
- * update the web interface in real-time.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetStatusAjaxPage extends JsonPage {
-
-       /** Date formatter. */
-       private static final DateFormat dateFormat = new SimpleDateFormat("MMM d, yyyy, HH:mm:ss");
-
-       /**
-        * Creates a new “get status” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetStatusAjaxPage(WebInterface webInterface) {
-               super("getStatus.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               /* load Sones. always return the status of the current Sone. */
-               Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
-               String loadSoneIds = request.getHttpRequest().getParam("soneIds");
-               if (loadSoneIds.length() > 0) {
-                       String[] soneIds = loadSoneIds.split(",");
-                       for (String soneId : soneIds) {
-                               /* just add it, we skip null further down. */
-                               sones.add(webInterface.getCore().getSone(soneId).orNull());
-                       }
-               }
-               ArrayNode jsonSones = new ArrayNode(instance);
-               for (Sone sone : sones) {
-                       if (sone == null) {
-                               continue;
-                       }
-                       jsonSones.add(createJsonSone(sone));
-               }
-               /* load notifications. */
-               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
-               Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
-               /* load new posts. */
-               Collection<Post> newPosts = webInterface.getNewPosts(getCurrentSone(request.getToadletContext(), false));
-
-               ArrayNode jsonPosts = new ArrayNode(instance);
-               for (Post post : newPosts) {
-                       ObjectNode jsonPost = new ObjectNode(instance);
-                       jsonPost.put("id", post.getId());
-                       jsonPost.put("sone", post.getSone().getId());
-                       jsonPost.put("recipient", post.getRecipientId().orNull());
-                       jsonPost.put("time", post.getTime());
-                       jsonPosts.add(jsonPost);
-               }
-               /* load new replies. */
-               Collection<PostReply> newReplies = webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false));
-
-               ArrayNode jsonReplies = new ArrayNode(instance);
-               for (PostReply reply : newReplies) {
-                       ObjectNode jsonReply = new ObjectNode(instance);
-                       jsonReply.put("id", reply.getId());
-                       jsonReply.put("sone", reply.getSone().getId());
-                       jsonReply.put("post", reply.getPostId());
-                       jsonReply.put("postSone", reply.getPost().get().getSone().getId());
-                       jsonReplies.add(jsonReply);
-               }
-               return createSuccessJsonObject().put("loggedIn", currentSone != null).put("options", createJsonOptions(currentSone)).put("sones", jsonSones).put("notificationHash", notifications.hashCode()).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a JSON object from the given Sone.
-        *
-        * @param sone
-        *            The Sone to convert to a JSON object
-        * @return The JSON representation of the given Sone
-        */
-       private JsonNode createJsonSone(Sone sone) {
-               ObjectNode jsonSone = new ObjectNode(instance);
-               jsonSone.put("id", sone.getId());
-               jsonSone.put("name", SoneAccessor.getNiceName(sone));
-               jsonSone.put("local", sone.getInsertUri() != null);
-               jsonSone.put("status", sone.getStatus().name());
-               jsonSone.put("modified", webInterface.getCore().isModifiedSone(sone));
-               jsonSone.put("locked", webInterface.getCore().isLocked(sone));
-               jsonSone.put("lastUpdatedUnknown", sone.getTime() == 0);
-               synchronized (dateFormat) {
-                       jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
-               }
-               jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, sone.getTime()).getText());
-               return jsonSone;
-       }
-
-       /**
-        * Creates a JSON object that contains all options that are currently in
-        * effect for the given Sone (or overall, if the given Sone is {@code null}
-        * ).
-        *
-        * @param currentSone
-        *            The current Sone (may be {@code null})
-        * @return The current options
-        */
-       private static JsonNode createJsonOptions(Sone currentSone) {
-               ObjectNode options = new ObjectNode(instance);
-               if (currentSone != null) {
-                       options.put("ShowNotification/NewSones", currentSone.getOptions().isShowNewSoneNotifications());
-                       options.put("ShowNotification/NewPosts", currentSone.getOptions().isShowNewPostNotifications());
-                       options.put("ShowNotification/NewReplies", currentSone.getOptions().isShowNewReplyNotifications());
-               }
-               return options;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
deleted file mode 100644 (file)
index 02fd0c1..0000000
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Sone - GetTimesAjaxPage.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.ajax;
-
-import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.base.Optional;
-
-/**
- * Ajax page that returns a formatted, relative timestamp for replies or posts.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetTimesAjaxPage extends JsonPage {
-
-       /** Formatter for tooltips. */
-       private static final DateFormat dateFormat = new SimpleDateFormat("MMM d, yyyy, HH:mm:ss");
-
-       /**
-        * Creates a new get times AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetTimesAjaxPage(WebInterface webInterface) {
-               super("getTimes.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String allIds = request.getHttpRequest().getParam("posts");
-               ObjectNode postTimes = new ObjectNode(instance);
-               if (allIds.length() > 0) {
-                       String[] ids = allIds.split(",");
-                       for (String id : ids) {
-                               Optional<Post> post = webInterface.getCore().getPost(id);
-                               if (!post.isPresent()) {
-                                       continue;
-                               }
-                               ObjectNode postTime = new ObjectNode(instance);
-                               Time time = getTime(post.get().getTime());
-                               postTime.put("timeText", time.getText());
-                               postTime.put("refreshTime", TimeUnit.MILLISECONDS.toSeconds(time.getRefresh()));
-                               synchronized (dateFormat) {
-                                       postTime.put("tooltip", dateFormat.format(new Date(post.get().getTime())));
-                               }
-                               postTimes.put(id, postTime);
-                       }
-               }
-               ObjectNode replyTimes = new ObjectNode(instance);
-               allIds = request.getHttpRequest().getParam("replies");
-               if (allIds.length() > 0) {
-                       String[] ids = allIds.split(",");
-                       for (String id : ids) {
-                               Optional<PostReply> reply = webInterface.getCore().getPostReply(id);
-                               if (!reply.isPresent()) {
-                                       continue;
-                               }
-                               ObjectNode replyTime = new ObjectNode(instance);
-                               Time time = getTime(reply.get().getTime());
-                               replyTime.put("timeText", time.getText());
-                               replyTime.put("refreshTime", TimeUnit.MILLISECONDS.toSeconds(time.getRefresh()));
-                               synchronized (dateFormat) {
-                                       replyTime.put("tooltip", dateFormat.format(new Date(reply.get().getTime())));
-                               }
-                               replyTimes.put(id, replyTime);
-                       }
-               }
-               return createSuccessJsonObject().put("postTimes", postTimes).put("replyTimes", replyTimes);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Returns the formatted relative time for a given time.
-        *
-        * @param time
-        *            The time to format the difference from (in milliseconds)
-        * @return The formatted age
-        */
-       private Time getTime(long time) {
-               return getTime(webInterface, time);
-       }
-
-       //
-       // STATIC METHODS
-       //
-
-       /**
-        * Returns the formatted relative time for a given time.
-        *
-        * @param webInterface
-        *            The Sone web interface (for l10n access)
-        * @param time
-        *            The time to format the difference from (in milliseconds)
-        * @return The formatted age
-        */
-       public static Time getTime(WebInterface webInterface, long time) {
-               if (time == 0) {
-                       return new Time(webInterface.getL10n().getString("View.Sone.Text.UnknownDate"), TimeUnit.HOURS.toMillis(12));
-               }
-               long age = System.currentTimeMillis() - time;
-               String text;
-               long refresh;
-               if (age < 0) {
-                       text = webInterface.getL10n().getString("View.Time.InTheFuture");
-                       refresh = TimeUnit.MINUTES.toMillis(5);
-               } else if (age < TimeUnit.SECONDS.toMillis(20)) {
-                       text = webInterface.getL10n().getString("View.Time.AFewSecondsAgo");
-                       refresh = TimeUnit.SECONDS.toMillis(10);
-               } else if (age < TimeUnit.SECONDS.toMillis(45)) {
-                       text = webInterface.getL10n().getString("View.Time.HalfAMinuteAgo");
-                       refresh = TimeUnit.SECONDS.toMillis(20);
-               } else if (age < TimeUnit.SECONDS.toMillis(90)) {
-                       text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
-                       refresh = TimeUnit.MINUTES.toMillis(1);
-               } else if (age < TimeUnit.MINUTES.toMillis(30)) {
-                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf(TimeUnit.MILLISECONDS.toMinutes(age + TimeUnit.SECONDS.toMillis(30))));
-                       refresh = TimeUnit.MINUTES.toMillis(1);
-               } else if (age < TimeUnit.MINUTES.toMillis(45)) {
-                       text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
-                       refresh = TimeUnit.MINUTES.toMillis(10);
-               } else if (age < TimeUnit.MINUTES.toMillis(90)) {
-                       text = webInterface.getL10n().getString("View.Time.AnHourAgo");
-                       refresh = TimeUnit.HOURS.toMillis(1);
-               } else if (age < TimeUnit.HOURS.toMillis(21)) {
-                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf(TimeUnit.MILLISECONDS.toHours(age + TimeUnit.MINUTES.toMillis(30))));
-                       refresh = TimeUnit.HOURS.toMillis(1);
-               } else if (age < TimeUnit.HOURS.toMillis(42)) {
-                       text = webInterface.getL10n().getString("View.Time.ADayAgo");
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(6)) {
-                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf(TimeUnit.MILLISECONDS.toDays(age + TimeUnit.HOURS.toMillis(12))));
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(11)) {
-                       text = webInterface.getL10n().getString("View.Time.AWeekAgo");
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(28)) {
-                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((TimeUnit.MILLISECONDS.toHours(age) + 84) / (7 * 24)));
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(42)) {
-                       text = webInterface.getL10n().getString("View.Time.AMonthAgo");
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(330)) {
-                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((TimeUnit.MILLISECONDS.toDays(age) + 15) / 30));
-                       refresh = TimeUnit.DAYS.toMillis(1);
-               } else if (age < TimeUnit.DAYS.toMillis(540)) {
-                       text = webInterface.getL10n().getString("View.Time.AYearAgo");
-                       refresh = TimeUnit.DAYS.toMillis(7);
-               } else {
-                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((long) ((TimeUnit.MILLISECONDS.toDays(age) + 182.64) / 365.28)));
-                       refresh = TimeUnit.DAYS.toMillis(7);
-               }
-               return new Time(text, refresh);
-       }
-
-       /**
-        * Container for a formatted time.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class Time {
-
-               /** The formatted time. */
-               private final String text;
-
-               /** The time after which to refresh the time. */
-               private final long refresh;
-
-               /**
-                * Creates a new formatted time container.
-                *
-                * @param text
-                *            The formatted time
-                * @param refresh
-                *            The time after which to refresh the time (in milliseconds)
-                */
-               public Time(String text, long refresh) {
-                       this.text = text;
-                       this.refresh = refresh;
-               }
-
-               /**
-                * Returns the formatted time.
-                *
-                * @return The formatted time
-                */
-               public String getText() {
-                       return text;
-               }
-
-               /**
-                * Returns the time after which to refresh the time.
-                *
-                * @return The time after which to refresh the time (in milliseconds)
-                */
-               public long getRefresh() {
-                       return refresh;
-               }
-
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               public String toString() {
-                       return text;
-               }
-
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
deleted file mode 100644 (file)
index 53e3e67..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Sone - GetTranslationPage.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.ajax;
-
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * Returns the translation for a given key as JSON object.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetTranslationPage extends JsonPage {
-
-       /**
-        * Creates a new translation page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public GetTranslationPage(WebInterface webInterface) {
-               super("getTranslation.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String key = request.getHttpRequest().getParam("key");
-               String translation = webInterface.getL10n().getString(key);
-               return createSuccessJsonObject().put("value", translation);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean needsFormPassword() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.java b/src/main/java/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.java
deleted file mode 100644 (file)
index 4612f0c..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * © 2013 xplosion interactive
- */
-
-package net.pterodactylus.sone.web.ajax;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.annotations.VisibleForTesting;
-
-/**
- * {@link JsonReturnObject} that signals an error has occured.
- *
- * @author <a href="mailto:d.roden@xplosion.de">David Roden</a>
- */
-public class JsonErrorReturnObject extends JsonReturnObject {
-
-       /** The error that has occured. */
-       @JsonProperty
-       private final String error;
-
-       /**
-        * Creates a new error JSON return object.
-        *
-        * @param error
-        *              The error that occured
-        */
-       public JsonErrorReturnObject(String error) {
-               super(false);
-               this.error = error;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the error that occured.
-        *
-        * @return The error that occured
-        */
-       @VisibleForTesting
-       public String getError() {
-               return error;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
deleted file mode 100644 (file)
index 289b5a4..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Sone - JsonPage.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.ajax;
-
-import static java.util.logging.Logger.getLogger;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.net.URI;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetPage;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.web.Page;
-import net.pterodactylus.util.web.Response;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import freenet.clients.http.SessionManager.Session;
-import freenet.clients.http.ToadletContext;
-
-/**
- * A JSON page is a specialized {@link Page} that will always return a JSON
- * object to the browser, e.g. for use with AJAX or other scripting frameworks.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public abstract class JsonPage implements FreenetPage {
-
-       /** The logger. */
-       private static final Logger logger = getLogger(JsonPage.class.getName());
-
-       /** The JSON serializer. */
-       private static final ObjectMapper objectMapper = new ObjectMapper();
-
-       /** The path of the page. */
-       private final String path;
-
-       /** The Sone web interface. */
-       protected final WebInterface webInterface;
-
-       /**
-        * Creates a new JSON page at the given path.
-        *
-        * @param path
-        *            The path of the page
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public JsonPage(String path, WebInterface webInterface) {
-               this.path = path;
-               this.webInterface = webInterface;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the current session, creating a new session if there is no
-        * current session.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       protected Session getCurrentSession(ToadletContext toadletContenxt) {
-               return webInterface.getCurrentSession(toadletContenxt);
-       }
-
-       /**
-        * Returns the current session, creating a new session if there is no
-        * current session and {@code create} is {@code true}.
-        *
-        * @param toadletContenxt
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if there is no current
-        *            session, {@code false} otherwise
-        * @return The current session, or {@code null} if there is no current
-        *         session
-        */
-       protected Session getCurrentSession(ToadletContext toadletContenxt, boolean create) {
-               return webInterface.getCurrentSession(toadletContenxt, create);
-       }
-
-       /**
-        * 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.getCurrentSone(toadletContext);
-       }
-
-       /**
-        * Returns the currently logged in Sone.
-        *
-        * @param toadletContext
-        *            The toadlet context
-        * @param create
-        *            {@code true} to create a new session if no session exists,
-        *            {@code false} to not create a new session
-        * @return The currently logged in Sone, or {@code null} if no Sone is
-        *         currently logged in
-        */
-       protected Sone getCurrentSone(ToadletContext toadletContext, boolean create) {
-               return webInterface.getCurrentSone(toadletContext, create);
-       }
-
-       //
-       // METHODS FOR SUBCLASSES TO OVERRIDE
-       //
-
-       /**
-        * This method is called to create the JSON object that is returned back to
-        * the browser.
-        *
-        * @param request
-        *            The request to handle
-        * @return The created JSON object
-        */
-       protected abstract JsonReturnObject createJsonObject(FreenetRequest request);
-
-       /**
-        * Returns whether this command needs the form password for authentication
-        * and to prevent abuse.
-        *
-        * @return {@code true} if the form password (given as “formPassword”) is
-        *         required, {@code false} otherwise
-        */
-       @SuppressWarnings("static-method")
-       protected boolean needsFormPassword() {
-               return true;
-       }
-
-       /**
-        * Returns whether this page requires the user to be logged in.
-        *
-        * @return {@code true} if the user needs to be logged in to use this page,
-        *         {@code false} otherwise
-        */
-       @SuppressWarnings("static-method")
-       protected boolean requiresLogin() {
-               return true;
-       }
-
-       //
-       // PROTECTED METHODS
-       //
-
-       /**
-        * Creates a success reply.
-        *
-        * @return A reply signaling success
-        */
-       protected static JsonReturnObject createSuccessJsonObject() {
-               return new JsonReturnObject(true);
-       }
-
-       /**
-        * Creates an error reply.
-        *
-        * @param error
-        *            The error that has occured
-        * @return The JSON object, signalling failure and the error code
-        */
-       protected static JsonReturnObject createErrorJsonObject(String error) {
-               return new JsonErrorReturnObject(error);
-       }
-
-       //
-       // PAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return path;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isPrefixPage() {
-               return false;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
-               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
-                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(objectMapper.writeValueAsString(new JsonErrorReturnObject("auth-required")));
-               }
-               if (needsFormPassword()) {
-                       String formPassword = request.getHttpRequest().getParam("formPassword");
-                       if (!webInterface.getFormPassword().equals(formPassword)) {
-                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(objectMapper.writeValueAsString(new JsonErrorReturnObject("auth-required")));
-                       }
-               }
-               if (requiresLogin()) {
-                       if (getCurrentSone(request.getToadletContext(), false) == null) {
-                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(objectMapper.writeValueAsString(new JsonErrorReturnObject("auth-required")));
-                       }
-               }
-               try {
-                       JsonReturnObject jsonObject = createJsonObject(request);
-                       return response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(objectMapper.writeValueAsString(jsonObject));
-               } catch (Exception e1) {
-                       logger.log(Level.WARNING, "Error executing JSON page!", e1);
-                       return response.setStatusCode(500).setStatusText(e1.getMessage()).setContentType("text/plain").write(dumpStackTrace(e1));
-               }
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean isLinkExcepted(URI link) {
-               return false;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Returns a byte array containing the stack trace of the given throwable.
-        *
-        * @param t
-        *            The throwable whose stack trace to dump into an array
-        * @return The array with the stack trace, or an empty array if the stack
-        *         trace could not be dumped
-        */
-       private static byte[] dumpStackTrace(Throwable t) {
-               ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-               OutputStreamWriter writer = null;
-               PrintWriter printWriter = null;
-               try {
-                       writer = new OutputStreamWriter(byteArrayOutputStream, "uTF-8");
-                       printWriter = new PrintWriter(writer);
-                       t.printStackTrace(printWriter);
-                       byteArrayOutputStream.flush();
-                       return byteArrayOutputStream.toByteArray();
-               } catch (IOException ioe1) {
-                       /* quite not possible. */
-                       return new byte[0];
-               } finally {
-                       Closer.close(printWriter);
-                       Closer.close(writer);
-                       Closer.close(byteArrayOutputStream);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/JsonReturnObject.java b/src/main/java/net/pterodactylus/sone/web/ajax/JsonReturnObject.java
deleted file mode 100644 (file)
index ca29f3c..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * © 2013 xplosion interactive
- */
-
-package net.pterodactylus.sone.web.ajax;
-
-import java.util.Map;
-
-import com.fasterxml.jackson.annotation.JsonAnyGetter;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.BooleanNode;
-import com.fasterxml.jackson.databind.node.IntNode;
-import com.fasterxml.jackson.databind.node.TextNode;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Maps;
-
-/**
- * JSON return object for AJAX requests.
- *
- * @author <a href="mailto:d.roden@xplosion.de">David Roden</a>
- */
-public class JsonReturnObject {
-
-       /** Whether the request was successful. */
-       @JsonProperty
-       private final boolean success;
-
-       /** The returned values. */
-       private final Map<String, JsonNode> content = Maps.newHashMap();
-
-       /**
-        * Creates a new JSON return object.
-        *
-        * @param success
-        *              {@code true} if the request was successful, {@code false} otherwise
-        */
-       public JsonReturnObject(boolean success) {
-               this.success = success;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns whether the request was successful.
-        *
-        * @return {@code true} if the request was successful, {@code false} otherwise
-        */
-       @VisibleForTesting
-       public boolean isSuccess() {
-               return success;
-       }
-
-       /**
-        * Returns the value stored under the given key.
-        *
-        * @param key
-        *              The key of the value to retrieve
-        * @return The value of the key, or {@code null} if there is no value for the
-        *         given key
-        */
-       @VisibleForTesting
-       public JsonNode get(String key) {
-               return content.get(key);
-       }
-
-       /**
-        * Returns the content of this object for serialization.
-        *
-        * @return The content of this object
-        */
-       @JsonAnyGetter
-       public Map<String, JsonNode> getContent() {
-               return content;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Stores the given value under the given key.
-        *
-        * @param key
-        *              The key under which to store the value
-        * @param value
-        *              The value to store
-        * @return This JSON return object
-        */
-       public JsonReturnObject put(String key, boolean value) {
-               return put(key, BooleanNode.valueOf(value));
-       }
-
-       /**
-        * Stores the given value under the given key.
-        *
-        * @param key
-        *              The key under which to store the value
-        * @param value
-        *              The value to store
-        * @return This JSON return object
-        */
-       public JsonReturnObject put(String key, int value) {
-               return put(key, new IntNode(value));
-       }
-
-       /**
-        * Stores the given value under the given key.
-        *
-        * @param key
-        *              The key under which to store the value
-        * @param value
-        *              The value to store
-        * @return This JSON return object
-        */
-       public JsonReturnObject put(String key, String value) {
-               return put(key, new TextNode(value));
-       }
-
-       /**
-        * Stores the given value under the given key.
-        *
-        * @param key
-        *              The key under which to store the value
-        * @param value
-        *              The value to store
-        * @return This JSON return object
-        */
-       public JsonReturnObject put(String key, JsonNode value) {
-               content.put(key, value);
-               return this;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java
deleted file mode 100644 (file)
index f088acb..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Sone - LikeAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user like a {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LikeAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “like post” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LikeAjaxPage(WebInterface webInterface) {
-               super("like.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String type = request.getHttpRequest().getParam("type", null);
-               String id = request.getHttpRequest().getParam(type, null);
-               if ((id == null) || (id.length() == 0)) {
-                       return createErrorJsonObject("invalid-" + type + "-id");
-               }
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               if ("post".equals(type)) {
-                       currentSone.addLikedPostId(id);
-                       webInterface.getCore().touchConfiguration();
-               } else if ("reply".equals(type)) {
-                       currentSone.addLikedReplyId(id);
-                       webInterface.getCore().touchConfiguration();
-               } else {
-                       return createErrorJsonObject("invalid-type");
-               }
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
deleted file mode 100644 (file)
index 7298bae..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - LockSoneAjaxPage.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.ajax;
-
-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;
-
-/**
- * Lets the user {@link Core#lockSone(Sone) lock} a {@link Sone}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LockSoneAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “lock Sone” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public LockSoneAjaxPage(WebInterface webInterface) {
-               super("lockSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String soneId = request.getHttpRequest().getParam("sone");
-               Sone sone = webInterface.getCore().getLocalSone(soneId);
-               if (sone == null) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               webInterface.getCore().lockSone(sone);
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java
deleted file mode 100644 (file)
index fff1975..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Sone - MarkAsKnownAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.core.Core;
-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 com.google.common.base.Optional;
-
-/**
- * AJAX page that lets the user mark a number of {@link Sone}s, {@link Post}s,
- * or {@link Reply}s as known.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class MarkAsKnownAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “mark as known” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public MarkAsKnownAjaxPage(WebInterface webInterface) {
-               super("markAsKnown.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String type = request.getHttpRequest().getParam("type");
-               if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
-                       return createErrorJsonObject("invalid-type");
-               }
-               String[] ids = request.getHttpRequest().getParam("id").split(" ");
-               Core core = webInterface.getCore();
-               for (String id : ids) {
-                       if (type.equals("post")) {
-                               Optional<Post> post = core.getPost(id);
-                               if (!post.isPresent()) {
-                                       continue;
-                               }
-                               core.markPostKnown(post.get());
-                       } else if (type.equals("reply")) {
-                               Optional<PostReply> reply = core.getPostReply(id);
-                               if (!reply.isPresent()) {
-                                       continue;
-                               }
-                               core.markReplyKnown(reply.get());
-                       } else if (type.equals("sone")) {
-                               Optional<Sone> sone = core.getSone(id);
-                               if (!sone.isPresent()) {
-                                       continue;
-                               }
-                               core.markSoneKnown(sone.get());
-                       }
-               }
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
deleted file mode 100644 (file)
index e8377f9..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Sone - MoveProfileFieldAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user move a profile field up or down.
- *
- * @see Profile#moveFieldUp(Field)
- * @see Profile#moveFieldDown(Field)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class MoveProfileFieldAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “move profile field” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public MoveProfileFieldAjaxPage(WebInterface webInterface) {
-               super("moveProfileField.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               Profile profile = currentSone.getProfile();
-               String fieldId = request.getHttpRequest().getParam("field");
-               Field field = profile.getFieldById(fieldId);
-               if (field == null) {
-                       return createErrorJsonObject("invalid-field-id");
-               }
-               String direction = request.getHttpRequest().getParam("direction");
-               try {
-                       if ("up".equals(direction)) {
-                               profile.moveFieldUp(field);
-                       } else if ("down".equals(direction)) {
-                               profile.moveFieldDown(field);
-                       } else {
-                               return createErrorJsonObject("invalid-direction");
-                       }
-               } catch (IllegalArgumentException iae1) {
-                       return createErrorJsonObject("not-possible");
-               }
-               currentSone.setProfile(profile);
-               webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java
deleted file mode 100644 (file)
index a186d46..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - TrustAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user trust a Sone.
- *
- * @see Core#trustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TrustAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “trust Sone” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public TrustAjaxPage(WebInterface webInterface) {
-               super("trustSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               if (!sone.isPresent()) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               webInterface.getCore().trustSone(currentSone, sone.get());
-               return createSuccessJsonObject().put("trustValue", webInterface.getCore().getPreferences().getPositiveTrust());
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
deleted file mode 100644 (file)
index 94dd268..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Sone - UnbookmarkAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-import com.google.common.base.Optional;
-
-/**
- * AJAX page that lets the user unbookmark a post.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnbookmarkAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new unbookmark AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnbookmarkAjaxPage(WebInterface webInterface) {
-               super("unbookmark.ajax", webInterface);
-       }
-
-       //
-       // JSONPAGE METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String id = request.getHttpRequest().getParam("post", null);
-               if ((id == null) || (id.length() == 0)) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               Optional<Post> post = webInterface.getCore().getPost(id);
-               if (post.isPresent()) {
-                       webInterface.getCore().unbookmarkPost(post.get());
-               }
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java
deleted file mode 100644 (file)
index 58af936..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Sone - UnfollowSoneAjaxPage.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.ajax;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-/**
- * AJAX page that lets a Sone unfollow another Sone.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnfollowSoneAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “unfollow Sone” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnfollowSoneAjaxPage(WebInterface webInterface) {
-               super("unfollowSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String soneId = request.getHttpRequest().getParam("sone");
-               if (!webInterface.getCore().getSone(soneId).isPresent()) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               webInterface.getCore().unfollowSone(currentSone, soneId);
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java
deleted file mode 100644 (file)
index 2eaaa68..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Sone - UnlikeAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user unlike a {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnlikeAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “unlike post” AJAX page.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnlikeAjaxPage(WebInterface webInterface) {
-               super("unlike.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String type = request.getHttpRequest().getParam("type", null);
-               String id = request.getHttpRequest().getParam(type, null);
-               if ((id == null) || (id.length() == 0)) {
-                       return createErrorJsonObject("invalid-" + type + "-id");
-               }
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               if ("post".equals(type)) {
-                       currentSone.removeLikedPostId(id);
-                       webInterface.getCore().touchConfiguration();
-               } else if ("reply".equals(type)) {
-                       currentSone.removeLikedReplyId(id);
-                       webInterface.getCore().touchConfiguration();
-               } else {
-                       return createErrorJsonObject("invalid-type");
-               }
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
deleted file mode 100644 (file)
index bf88371..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sone - UnlockSoneAjaxPage.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.ajax;
-
-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;
-
-/**
- * Lets the user {@link Core#unlockSone(Sone) unlock} a {@link Sone}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnlockSoneAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “unlock Sone” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UnlockSoneAjaxPage(WebInterface webInterface) {
-               super("unlockSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               String soneId = request.getHttpRequest().getParam("sone");
-               Sone sone = webInterface.getCore().getLocalSone(soneId);
-               if (sone == null) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               webInterface.getCore().unlockSone(sone);
-               return createSuccessJsonObject();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected boolean requiresLogin() {
-               return false;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java
deleted file mode 100644 (file)
index 644dfd6..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Sone - UntrustAjaxPage.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.ajax;
-
-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;
-
-/**
- * AJAX page that lets the user untrust a Sone.
- *
- * @see Core#untrustSone(Sone, Sone)
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UntrustAjaxPage extends JsonPage {
-
-       /**
-        * Creates a new “untrust Sone” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public UntrustAjaxPage(WebInterface webInterface) {
-               super("untrustSone.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonReturnObject createJsonObject(FreenetRequest request) {
-               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               String soneId = request.getHttpRequest().getParam("sone");
-               Optional<Sone> sone = webInterface.getCore().getSone(soneId);
-               if (!sone.isPresent()) {
-                       return createErrorJsonObject("invalid-sone-id");
-               }
-               webInterface.getCore().untrustSone(currentSone, sone.get());
-               return createSuccessJsonObject().put("trustValue", (String) null);
-       }
-
-}
index 4c20e1f..9cfd026 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web.page;
 
+import static java.lang.String.format;
 import static java.util.logging.Logger.getLogger;
 
 import java.io.IOException;
@@ -118,7 +119,7 @@ public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+       public final Response handleRequest(FreenetRequest request, Response response) throws IOException {
                String redirectTarget = getRedirectTarget(request);
                if (redirectTarget != null) {
                        return new RedirectResponse(redirectTarget);
@@ -157,7 +158,7 @@ public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback {
                        long start = System.nanoTime();
                        processTemplate(request, templateContext);
                        long finish = System.nanoTime();
-                       logger.log(Level.FINEST, String.format("Template was rendered in %.2fms.", (finish - start) / 1000000.0));
+                       logger.log(Level.FINEST, format("Template was rendered in %.2fms.", (finish - start) / 1000000.0));
                } catch (RedirectException re1) {
                        return new RedirectResponse(re1.getTarget());
                }
@@ -312,6 +313,11 @@ public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback {
                        return target;
                }
 
+               @Override
+               public String toString() {
+                       return format("RedirectException{target='%s'}", target);
+               }
+
        }
 
 }
index 222aa72..17a1bab 100644 (file)
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
 
+import net.pterodactylus.sone.utils.AutoCloseableBucket;
 import net.pterodactylus.util.web.Header;
 import net.pterodactylus.util.web.Method;
 import net.pterodactylus.util.web.Page;
@@ -33,9 +34,7 @@ import freenet.clients.http.Toadlet;
 import freenet.clients.http.ToadletContext;
 import freenet.clients.http.ToadletContextClosedException;
 import freenet.support.MultiValueTable;
-import freenet.support.api.Bucket;
 import freenet.support.api.HTTPRequest;
-import freenet.support.io.Closer;
 
 /**
  * {@link Toadlet} implementation that is wrapped around a {@link Page}.
@@ -145,31 +144,18 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFil
         *             if the toadlet context is closed
         */
        private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException {
-               Bucket pageBucket = null;
-               OutputStream pageBucketOutputStream = null;
-               Response pageResponse;
-               try {
-                       pageBucket = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
-                       pageBucketOutputStream = pageBucket.getOutputStream();
-                       pageResponse = page.handleRequest(pageRequest, new Response(pageBucketOutputStream));
-               } catch (IOException ioe1) {
-                       Closer.close(pageBucket);
-                       throw ioe1;
-               } finally {
-                       Closer.close(pageBucketOutputStream);
-               }
-               MultiValueTable<String, String> headers = new MultiValueTable<String, String>();
-               if (pageResponse.getHeaders() != null) {
-                       for (Header header : pageResponse.getHeaders()) {
-                               for (String value : header) {
-                                       headers.put(header.getName(), value);
+               try (AutoCloseableBucket pageBucket = new AutoCloseableBucket(pageRequest.getToadletContext().getBucketFactory().makeBucket(-1));
+                    OutputStream pageBucketOutputStream = pageBucket.getBucket().getOutputStream()) {
+                       Response pageResponse = page.handleRequest(pageRequest, new Response(pageBucketOutputStream));
+                       MultiValueTable<String, String> headers = new MultiValueTable<String, String>();
+                       if (pageResponse.getHeaders() != null) {
+                               for (Header header : pageResponse.getHeaders()) {
+                                       for (String value : header) {
+                                               headers.put(header.getName(), value);
+                                       }
                                }
                        }
-               }
-               try {
-                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket);
-               } finally {
-                       Closer.close(pageBucket);
+                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket.getBucket());
                }
        }
 
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt
new file mode 100644 (file)
index 0000000..d01aeb4
--- /dev/null
@@ -0,0 +1,116 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.base.Ticker
+import com.google.common.cache.Cache
+import com.google.common.cache.CacheBuilder
+import freenet.keys.FreenetURI
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.TextNode
+import java.io.ByteArrayInputStream
+import java.net.URLDecoder
+import java.nio.charset.Charset
+import java.text.Normalizer
+import java.util.concurrent.TimeUnit.MINUTES
+import javax.activation.MimeType
+import javax.imageio.ImageIO
+import javax.inject.Inject
+
+/**
+ * [ElementLoader] implementation that uses a simple Guava [com.google.common.cache.Cache].
+ */
+class DefaultElementLoader(private val freenetInterface: FreenetInterface, ticker: Ticker): ElementLoader {
+
+       @Inject constructor(freenetInterface: FreenetInterface): this(freenetInterface, Ticker.systemTicker())
+
+       private val loadingLinks: Cache<String, Boolean> = CacheBuilder.newBuilder().build<String, Boolean>()
+       private val failureCache: Cache<String, Boolean> = CacheBuilder.newBuilder().ticker(ticker).expireAfterWrite(30, MINUTES).build<String, Boolean>()
+       private val elementCache: Cache<String, LinkedElement> = CacheBuilder.newBuilder().build<String, LinkedElement>()
+       private val callback = object: FreenetInterface.BackgroundFetchCallback {
+               override fun shouldCancel(uri: FreenetURI, mimeType: String, size: Long): Boolean {
+                       return (size > 2097152) || (!mimeType.startsWith("image/") && !mimeType.startsWith("text/html"))
+               }
+
+               override fun loaded(uri: FreenetURI, mimeTypeText: String, data: ByteArray) {
+                       MimeType(mimeTypeText).also { mimeType ->
+                               when {
+                                       mimeType.primaryType == "image" -> {
+                                               ByteArrayInputStream(data).use {
+                                                       ImageIO.read(it)
+                                               }?.let {
+                                                       elementCache.get(uri.toString().decode().normalize()) {
+                                                               LinkedElement(uri.toString(), properties = mapOf("type" to "image", "size" to data.size, "sizeHuman" to data.size.human))
+                                                       }
+                                               }
+                                       }
+                                       mimeType.baseType == "text/html" -> {
+                                               val document = Jsoup.parse(data.toString(Charset.forName(mimeType.getParameter("charset") ?: "UTF-8")))
+                                               elementCache.get(uri.toString().decode().normalize()) {
+                                                       LinkedElement(uri.toString(), properties = mapOf(
+                                                                       "type" to "html", "size" to data.size, "sizeHuman" to data.size.human,
+                                                                       "title" to document.title().emptyToNull,
+                                                                       "description" to (document.metaDescription ?: document.firstNonHeadingParagraph)
+                                                       ))
+                                               }
+                                       }
+                               }
+                               removeLoadingLink(uri)
+                       }
+               }
+
+               private val String?.emptyToNull get() = if (this == "") null else this
+
+               private val Document.metaDescription: String?
+                       get() = head().getElementsByTag("meta")
+                                       .map { it.attr("name") to it.attr("content") }
+                                       .firstOrNull { it.first == "description" }
+                                       ?.second
+
+               private val Document.firstNonHeadingParagraph: String?
+                       get() = body().children()
+                                       .filter { it.children().all { it is TextNode } }
+                                       .map { it to it.text() }
+                                       .filterNot { it.second == "" }
+                                       .firstOrNull { !it.first.tagName().startsWith("h", ignoreCase = true) }
+                                       ?.second
+
+               private val Int.human get() = when (this) {
+                       in 0..1023 -> "$this B"
+                       in 1024..1048575 -> "${this / 1024} KiB"
+                       in 1048576..1073741823 -> "${this / 1048576} MiB"
+                       else -> "${this / 1073741824} GiB"
+               }
+
+               override fun failed(uri: FreenetURI) {
+                       failureCache.put(uri.toString().decode().normalize(), true)
+                       removeLoadingLink(uri)
+               }
+
+               private fun removeLoadingLink(uri: FreenetURI) {
+                       synchronized(loadingLinks) {
+                               loadingLinks.invalidate(uri.toString().decode().normalize())
+                       }
+               }
+       }
+
+       override fun loadElement(link: String): LinkedElement {
+               val normalizedLink = link.decode().normalize()
+               synchronized(loadingLinks) {
+                       elementCache.getIfPresent(normalizedLink)?.run {
+                               return this
+                       }
+                       failureCache.getIfPresent(normalizedLink)?.run {
+                               return LinkedElement(link, failed = true)
+                       }
+                       if (loadingLinks.getIfPresent(normalizedLink) == null) {
+                               loadingLinks.put(normalizedLink, true)
+                               freenetInterface.startFetch(FreenetURI(link), callback)
+                       }
+               }
+               return LinkedElement(link, loading = true)
+       }
+
+       private fun String.decode() = URLDecoder.decode(this, "UTF-8")!!
+       private fun String.normalize() = Normalizer.normalize(this, Normalizer.Form.NFC)!!
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt
new file mode 100644 (file)
index 0000000..aec021d
--- /dev/null
@@ -0,0 +1,15 @@
+package net.pterodactylus.sone.core
+
+import com.google.inject.ImplementedBy
+
+/**
+ * Component that loads images and supplies information about them.
+ */
+@ImplementedBy(DefaultElementLoader::class)
+interface ElementLoader {
+
+       fun loadElement(link: String): LinkedElement
+
+}
+
+data class LinkedElement(val link: String, val failed: Boolean = false, val loading: Boolean = false, val properties: Map<String, Any?> = emptyMap())
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/L10nText.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/L10nText.kt
new file mode 100644 (file)
index 0000000..0c975be
--- /dev/null
@@ -0,0 +1,6 @@
+package net.pterodactylus.sone.freenet
+
+/**
+ * Container for an l10n key and optional values.
+ */
+data class L10nText(val text: String, val parameters: List<Any?> = emptyList())
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt b/src/main/kotlin/net/pterodactylus/sone/main/FreenetModule.kt
new file mode 100644 (file)
index 0000000..a8da595
--- /dev/null
@@ -0,0 +1,25 @@
+package net.pterodactylus.sone.main
+
+import com.google.inject.Binder
+import com.google.inject.Module
+import com.google.inject.Provides
+import freenet.client.HighLevelSimpleClient
+import freenet.node.Node
+import freenet.pluginmanager.PluginRespirator
+import javax.inject.Singleton
+
+/**
+ * Guice [Module] that supplies some objects that are in fact supplied by the Freenet node.
+ */
+class FreenetModule(private val pluginRespirator: PluginRespirator): Module {
+
+       override fun configure(binder: Binder): Unit = binder.run {
+               bind(PluginRespirator::class.java).toProvider { pluginRespirator }
+               pluginRespirator.node!!.let { node -> bind(Node::class.java).toProvider { node } }
+               bind(HighLevelSimpleClient::class.java).toProvider { pluginRespirator.hlSimpleClient!! }
+       }
+
+       @Provides @Singleton
+       fun getSessionManager() = pluginRespirator.getSessionManager("Sone")!!
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/NoArg.kt b/src/main/kotlin/net/pterodactylus/sone/main/NoArg.kt
new file mode 100644 (file)
index 0000000..eccf929
--- /dev/null
@@ -0,0 +1,7 @@
+package net.pterodactylus.sone.main
+
+/**
+ * Annotation for class that will have a no-argument constructor artificially generated by
+ * the no-arg Kotlin compiler plugin.
+ */
+annotation class NoArg
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/VersionParser.kt b/src/main/kotlin/net/pterodactylus/sone/main/VersionParser.kt
new file mode 100644 (file)
index 0000000..4ba14f8
--- /dev/null
@@ -0,0 +1,17 @@
+package net.pterodactylus.sone.main
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+
+@JvmOverloads
+fun parseVersion(file: String = "/version.yaml"): Version? =
+               Version::class.java.getResourceAsStream(file)?.use {
+                       objectMapper.readValue(it, Version::class.java)
+               }
+
+val parsedVersion by lazy { parseVersion() }
+
+private val objectMapper = ObjectMapper(YAMLFactory())
+
+@NoArg
+data class Version(val id: String, val nice: String)
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt
new file mode 100644 (file)
index 0000000..4536c04
--- /dev/null
@@ -0,0 +1,53 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.utils.asTemplate
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import java.io.StringWriter
+import javax.inject.Inject
+
+/**
+ * Renders all kinds of [LinkedElement]s.
+ */
+class LinkedElementRenderFilter @Inject constructor(private val templateContextFactory: TemplateContextFactory): Filter {
+
+       companion object {
+               private val loadedImageTemplate = """<%include linked/image.html>""".asTemplate()
+               private val loadedHtmlPageTemplate = """<%include linked/html-page.html>""".asTemplate()
+               private val notLoadedImageTemplate = """<%include linked/notLoaded.html>""".asTemplate()
+       }
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?) =
+                       when {
+                               data is LinkedElement && data.loading -> renderNotLoadedLinkedElement(data)
+                               data is LinkedElement && data.properties["type"] == "image" -> renderLinkedImage(data)
+                               data is LinkedElement && data.properties["type"] == "html" -> renderHtmlPage(data)
+                               else -> null
+                       }
+
+       private fun renderLinkedImage(linkedElement: LinkedElement) =
+                       StringWriter().use {
+                               val templateContext = templateContextFactory.createTemplateContext()
+                               templateContext["link"] = linkedElement.link
+                               it.also { loadedImageTemplate.render(templateContext, it) }
+                       }.toString()
+
+       private fun renderHtmlPage(linkedElement: LinkedElement) =
+                       StringWriter().use {
+                               val templateContext = templateContextFactory.createTemplateContext()
+                               templateContext["link"] = linkedElement.link
+                               templateContext["title"] = linkedElement.properties["title"] ?: "No title"
+                               templateContext["description"] = linkedElement.properties["description"] ?: "No description"
+                               it.also { loadedHtmlPageTemplate.render(templateContext, it) }
+                       }.toString()
+
+       private fun renderNotLoadedLinkedElement(linkedElement: LinkedElement) =
+                       StringWriter().use {
+                               val templateContext = templateContextFactory.createTemplateContext()
+                               templateContext["link"] = linkedElement.link
+                               it.also { notLoadedImageTemplate.render(templateContext, it) }
+                       }.toString()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt
new file mode 100644 (file)
index 0000000..c9414ea
--- /dev/null
@@ -0,0 +1,57 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.ALWAYS
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.FOLLOWED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.MANUALLY_TRUSTED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.NEVER
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet elements
+ * with [LinkedElement]s.
+ */
+class LinkedElementsFilter(private val elementLoader: ElementLoader) : Filter {
+
+       @Suppress("UNCHECKED_CAST")
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?) =
+                       if (showLinkedImages(templateContext?.get("currentSone") as Sone?, parameters?.get("sone") as Sone?)) {
+                               (data as? Iterable<Part>)
+                                               ?.filterIsInstance<FreenetLinkPart>()
+                                               ?.map { elementLoader.loadElement(it.link) }
+                                               ?.filter { !it.failed }
+                                               ?: listOf<LinkedElement>()
+                       } else {
+                               listOf<LinkedElement>()
+                       }
+
+       private fun showLinkedImages(currentSone: Sone?, sone: Sone?): Boolean {
+               return (currentSone != null) && (sone != null) && ((currentSone == sone) || currentSoneAllowsImagesFromSone(currentSone, sone))
+       }
+
+       private fun currentSoneAllowsImagesFromSone(currentSone: Sone, externalSone: Sone) =
+                       when (currentSone.options.loadLinkedImages) {
+                               NEVER -> false
+                               MANUALLY_TRUSTED -> externalSone.isLocal || currentSone.explicitelyTrusts(externalSone)
+                               FOLLOWED -> externalSone.isLocal || currentSone.hasFriend(externalSone.id)
+                               TRUSTED -> externalSone.isLocal || currentSone.implicitelyTrusts(externalSone)
+                               ALWAYS -> true
+                       }
+
+       private fun Sone.implicitelyTrusts(other: Sone): Boolean {
+               val explicitTrust = other.identity.getTrust(this.identity as OwnIdentity)?.explicit
+               val implicitTrust = other.identity.getTrust(this.identity as OwnIdentity)?.implicit
+               return ((explicitTrust != null) && (explicitTrust > 0)) || ((explicitTrust == null) && (implicitTrust != null) && (implicitTrust > 0))
+       }
+
+       private fun Sone.explicitelyTrusts(other: Sone) =
+                       other.identity.getTrust(this.identity as OwnIdentity)?.explicit ?: -1 > 0
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/ParserFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/ParserFilter.kt
new file mode 100644 (file)
index 0000000..cec5d56
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.SoneTextParser
+import net.pterodactylus.sone.text.SoneTextParserContext
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Parses a [String] into a number of [Part]s.
+ */
+@Singleton
+class ParserFilter @Inject constructor(private val core: Core, private val soneTextParser: SoneTextParser) : Filter {
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?): Any? {
+               val text = data?.toString() ?: return listOf<Part>()
+               val soneParameter = parameters?.get("sone")
+               val sone = when (soneParameter) {
+                       is String -> core.getSone(soneParameter).orNull()
+                       is Sone -> soneParameter
+                       else -> null
+               }
+               val context = SoneTextParserContext(sone)
+               return soneTextParser.parse(text, context)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/RenderFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/RenderFilter.kt
new file mode 100644 (file)
index 0000000..43331c3
--- /dev/null
@@ -0,0 +1,121 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.text.FreemailPart
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.PlainTextPart
+import net.pterodactylus.sone.text.PostPart
+import net.pterodactylus.sone.text.SonePart
+import net.pterodactylus.sone.text.SoneTextParser
+import net.pterodactylus.sone.text.SoneTextParserContext
+import net.pterodactylus.sone.utils.asTemplate
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import java.io.StringWriter
+import java.io.Writer
+import java.net.URLEncoder
+
+/**
+ * Renders a number of pre-parsed [Part] into a [String].
+ *
+ * @author [David ‘Bombe’ Roden](mailto:bombe@pterodactylus.net)
+ */
+class RenderFilter(private val core: Core, private val templateContextFactory: TemplateContextFactory) : Filter {
+
+       companion object {
+               private val plainTextTemplate = "<%text|html>".asTemplate()
+               private val linkTemplate = "<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>".asTemplate()
+       }
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?): Any? {
+               @Suppress("UNCHECKED_CAST")
+               val parts = data as? Iterable<Part> ?: return null
+               val parsedTextWriter = StringWriter()
+               render(parsedTextWriter, parts)
+               return parsedTextWriter.toString()
+       }
+
+       private fun render(writer: Writer, parts: Iterable<Part>) {
+               parts.forEach { render(writer, it) }
+       }
+
+       private fun render(writer: Writer, part: Part) {
+               @Suppress("UNCHECKED_CAST")
+               when (part) {
+                       is PlainTextPart -> render(writer, part)
+                       is FreenetLinkPart -> render(writer, part)
+                       is LinkPart -> render(writer, part)
+                       is SonePart -> render(writer, part)
+                       is PostPart -> render(writer, part)
+                       is FreemailPart -> render(writer, part)
+               }
+       }
+
+       private fun render(writer: Writer, plainTextPart: PlainTextPart) {
+               val templateContext = templateContextFactory.createTemplateContext()
+               templateContext.set("text", plainTextPart.text)
+               plainTextTemplate.render(templateContext, writer)
+       }
+
+       private fun render(writer: Writer, freenetLinkPart: FreenetLinkPart) {
+               renderLink(writer, "/${freenetLinkPart.link}", freenetLinkPart.text, freenetLinkPart.title, if (freenetLinkPart.trusted) "freenet-trusted" else "freenet")
+       }
+
+       private fun render(writer: Writer, linkPart: LinkPart) {
+               renderLink(writer, "/external-link/?_CHECKED_HTTP_=${linkPart.link.urlEncode()}", linkPart.text, linkPart.title, "internet")
+       }
+
+       private fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8")
+
+       private fun render(writer: Writer, sonePart: SonePart) {
+               if (sonePart.sone.name != null) {
+                       renderLink(writer, "viewSone.html?sone=${sonePart.sone.id}", SoneAccessor.getNiceName(sonePart.sone), SoneAccessor.getNiceName(sonePart.sone), "in-sone")
+               } else {
+                       renderLink(writer, "/WebOfTrust/ShowIdentity?id=${sonePart.sone.id}", sonePart.sone.id, sonePart.sone.id, "in-sone")
+               }
+       }
+
+       private fun render(writer: Writer, postPart: PostPart) {
+               val parser = SoneTextParser(core, core)
+               val parserContext = SoneTextParserContext(postPart.post.sone)
+               val parts = parser.parse(postPart.post.text, parserContext)
+               val excerpt = StringBuilder()
+               for (part in parts) {
+                       excerpt.append(part.text)
+                       if (excerpt.length > 20) {
+                               val lastSpace = excerpt.lastIndexOf(" ", 20)
+                               if (lastSpace > -1) {
+                                       excerpt.setLength(lastSpace)
+                               } else {
+                                       excerpt.setLength(20)
+                               }
+                               excerpt.append("…")
+                               break
+                       }
+               }
+               renderLink(writer, "viewPost.html?post=${postPart.post.id}", excerpt.toString(), SoneAccessor.getNiceName(postPart.post.sone), "in-sone")
+       }
+
+       private fun render(writer: Writer, freemailPart: FreemailPart) {
+               val sone = core.getSone(freemailPart.identityId)
+               val soneName = sone.transform(SoneAccessor::getNiceName).or(freemailPart.identityId)
+               renderLink(writer,
+                               "/Freemail/NewMessage?to=${freemailPart.identityId}",
+                               "${freemailPart.emailLocalPart}@$soneName.freemail",
+                               "$soneName\n${freemailPart.emailLocalPart}@${freemailPart.freemailId}.freemail",
+                               "in-sone")
+       }
+
+       private fun renderLink(writer: Writer, link: String, text: String, title: String, cssClass: String) {
+               val templateContext = templateContextFactory.createTemplateContext()
+               templateContext["cssClass"] = cssClass
+               templateContext["link"] = link
+               templateContext["text"] = text
+               templateContext["title"] = title
+               linkTemplate.render(templateContext, writer)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/ShortenFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/ShortenFilter.kt
new file mode 100644 (file)
index 0000000..de7a930
--- /dev/null
@@ -0,0 +1,54 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.PlainTextPart
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import java.util.*
+
+/**
+ * [Filter] that shortens a number of [Part]s in order to restrict the maximum visible text length.
+ */
+class ShortenFilter : Filter {
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?): Any? {
+               @Suppress("UNCHECKED_CAST")
+               val parts = data as? Iterable<Part> ?: return null
+               val length = parameters?.parseInt("length") ?: -1
+               val cutOffLength = parameters?.parseInt("cut-off-length") ?: length
+               if (length > -1) {
+                       var allPartsLength = 0
+                       val shortenedParts = ArrayList<Part>()
+                       for (part in parts) {
+                               if (part is PlainTextPart) {
+                                       val longText = part.text
+                                       if (allPartsLength < cutOffLength) {
+                                               if (allPartsLength + longText.length > cutOffLength) {
+                                                       shortenedParts.add(PlainTextPart(longText.substring(0, cutOffLength - allPartsLength) + "…"))
+                                               } else {
+                                                       shortenedParts.add(part)
+                                               }
+                                       }
+                                       allPartsLength += longText.length
+                               } else if (part is LinkPart) {
+                                       if (allPartsLength < cutOffLength) {
+                                               shortenedParts.add(part)
+                                       }
+                                       allPartsLength += part.text.length
+                               } else {
+                                       if (allPartsLength < cutOffLength) {
+                                               shortenedParts.add(part)
+                                       }
+                               }
+                       }
+                       if (allPartsLength > length) {
+                               return shortenedParts
+                       }
+               }
+               return parts
+       }
+
+       private fun Map<String, Any?>.parseInt(key: String) = this[key]?.toString()?.toInt()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/FreenetLinkPart.kt b/src/main/kotlin/net/pterodactylus/sone/text/FreenetLinkPart.kt
new file mode 100644 (file)
index 0000000..3744fd7
--- /dev/null
@@ -0,0 +1,12 @@
+package net.pterodactylus.sone.text
+
+/**
+ * [LinkPart] implementation that stores an additional attribute: if the
+ * link is an SSK or USK link and the post was created by an identity that owns
+ * the keyspace in question.
+ */
+data class FreenetLinkPart(val link: String, override val text: String, val title: String, val trusted: Boolean) : Part {
+
+       constructor(link: String, text: String, trusted: Boolean) : this(link, text, link, trusted)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/LinkPart.kt b/src/main/kotlin/net/pterodactylus/sone/text/LinkPart.kt
new file mode 100644 (file)
index 0000000..7099b93
--- /dev/null
@@ -0,0 +1,12 @@
+package net.pterodactylus.sone.text
+
+/**
+ * {@link Part} implementation that can hold a link. A link contains of three
+ * attributes: the link itself, the text that is shown instead of the link, and
+ * an explanatory text that can be displayed e.g. as a tooltip.
+ */
+data class LinkPart(val link: String, override val text: String, val title: String) : Part {
+
+       constructor(link: String, text: String) : this(link, text, link)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/Part.kt b/src/main/kotlin/net/pterodactylus/sone/text/Part.kt
new file mode 100644 (file)
index 0000000..8633ec1
--- /dev/null
@@ -0,0 +1,11 @@
+package net.pterodactylus.sone.text
+
+/**
+ * A part is a single piece of information that can be displayed as a single
+ * element. How the part is displayed is not part of the [Part] specification.
+ */
+interface Part {
+
+       val text: String
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/PlainTextPart.kt b/src/main/kotlin/net/pterodactylus/sone/text/PlainTextPart.kt
new file mode 100644 (file)
index 0000000..4a495b4
--- /dev/null
@@ -0,0 +1,8 @@
+package net.pterodactylus.sone.text
+
+/**
+ * [Part] implementation that holds a single piece of text.
+ *
+ * @author [David Roden](mailto:d.roden@emetriq.com)
+ */
+data class PlainTextPart(override val text: String) : Part
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SonePart.kt b/src/main/kotlin/net/pterodactylus/sone/text/SonePart.kt
new file mode 100644 (file)
index 0000000..fd22474
--- /dev/null
@@ -0,0 +1,13 @@
+package net.pterodactylus.sone.text
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.template.SoneAccessor
+
+/**
+ * [Part] implementation that stores a reference to a [Sone].
+ */
+data class SonePart(val sone: Sone) : Part {
+
+       override val text: String = SoneAccessor.getNiceName(sone)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/TimeText.kt b/src/main/kotlin/net/pterodactylus/sone/text/TimeText.kt
new file mode 100644 (file)
index 0000000..740e03e
--- /dev/null
@@ -0,0 +1,8 @@
+package net.pterodactylus.sone.text
+
+import net.pterodactylus.sone.freenet.L10nText
+
+/**
+ * Container for an l10n key and a refresh time.
+ */
+data class TimeText(val l10nText: L10nText, val refreshTime: Long)
diff --git a/src/main/kotlin/net/pterodactylus/sone/text/TimeTextConverter.kt b/src/main/kotlin/net/pterodactylus/sone/text/TimeTextConverter.kt
new file mode 100644 (file)
index 0000000..ae79fe5
--- /dev/null
@@ -0,0 +1,50 @@
+package net.pterodactylus.sone.text
+
+import net.pterodactylus.sone.freenet.L10nText
+import java.util.concurrent.TimeUnit.DAYS
+import java.util.concurrent.TimeUnit.HOURS
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+/**
+ * Converts a time (in Java milliseconds) to an L10n key and a refresh time.
+ */
+class TimeTextConverter(private val timeSuppler: () -> Long = { System.currentTimeMillis() }) {
+
+       fun getTimeText(time: Long): TimeText {
+               val age = timeSuppler.invoke() - time
+               return when {
+                       time == 0L -> TimeText(L10nText("View.Sone.Text.UnknownDate"), 12.hours())
+                       age < 0 -> TimeText(L10nText("View.Time.InTheFuture"), 5.minutes())
+                       age < 20.seconds() -> TimeText(L10nText("View.Time.AFewSecondsAgo"), 10.seconds())
+                       age < 45.seconds() -> TimeText(L10nText("View.Time.HalfAMinuteAgo"), 20.seconds())
+                       age < 90.seconds() -> TimeText(L10nText("View.Time.AMinuteAgo"), 1.minutes())
+                       age < 30.minutes() -> TimeText(L10nText("View.Time.XMinutesAgo", listOf((age + 30.seconds()).toMinutes())), 1.minutes())
+                       age < 45.minutes() -> TimeText(L10nText("View.Time.HalfAnHourAgo"), 10.minutes())
+                       age < 90.minutes() -> TimeText(L10nText("View.Time.AnHourAgo"), 1.hours())
+                       age < 21.hours() -> TimeText(L10nText("View.Time.XHoursAgo", listOf((age + 30.minutes()).toHours())), 1.hours())
+                       age < 42.hours() -> TimeText(L10nText("View.Time.ADayAgo"), 1.days())
+                       age < 6.days() -> TimeText(L10nText("View.Time.XDaysAgo", listOf((age + 12.hours()).toDays())), 1.days())
+                       age < 11.days() -> TimeText(L10nText("View.Time.AWeekAgo"), 1.days())
+                       age < 28.days() -> TimeText(L10nText("View.Time.XWeeksAgo", listOf((age + 3.days() + 12.hours()).toWeeks())), 1.days())
+                       age < 42.days() -> TimeText(L10nText("View.Time.AMonthAgo"), 1.days())
+                       age < 330.days() -> TimeText(L10nText("View.Time.XMonthsAgo", listOf((age + 15.days()).toMonths())), 1.days())
+                       age < 540.days() -> TimeText(L10nText("View.Time.AYearAgo"), 7.days())
+                       else -> TimeText(L10nText("View.Time.XYearsAgo", listOf((age + 182.days() + 12.hours()).toYears())), 7.days())
+               }
+       }
+
+       private fun Long.toMinutes() = MILLISECONDS.toMinutes(this)
+       private fun Long.toHours() = MILLISECONDS.toHours(this)
+       private fun Long.toDays() = MILLISECONDS.toDays(this)
+       private fun Long.toWeeks() = MILLISECONDS.toDays(this) / 7
+       private fun Long.toMonths() = MILLISECONDS.toDays(this) / 30
+       private fun Long.toYears() = MILLISECONDS.toDays(this) / 365
+       private fun Int.seconds() = SECONDS.toMillis(this.toLong())
+       private fun Int.minutes() = MINUTES.toMillis(this.toLong())
+       private fun Int.hours() = HOURS.toMillis(this.toLong())
+       private fun Int.days() = DAYS.toMillis(this.toLong())
+
+}
+
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt b/src/main/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucket.kt
new file mode 100644 (file)
index 0000000..58c181f
--- /dev/null
@@ -0,0 +1,11 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.Bucket
+
+class AutoCloseableBucket(val bucket: Bucket) : AutoCloseable {
+
+       override fun close() {
+               bucket.free()
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Booleans.kt
new file mode 100644 (file)
index 0000000..bfcb319
--- /dev/null
@@ -0,0 +1,11 @@
+package net.pterodactylus.sone.utils
+
+/**
+ * Returns the value of [block] if `this` is true, returns `null` otherwise.
+ */
+fun <R> Boolean.ifTrue(block: () -> R): R? = if (this) block() else null
+
+/**
+ * Returns the value of [block] if `this` is false, returns `null` otherwise.
+ */
+fun <R> Boolean.ifFalse(block: () -> R): R? = if (!this) block() else null
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Buckets.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Buckets.kt
new file mode 100644 (file)
index 0000000..99924ad
--- /dev/null
@@ -0,0 +1,9 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.Bucket
+
+fun <R> Bucket.use(block: (Bucket) -> R): R = try {
+       block(this)
+} finally {
+       free()
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Json.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Json.kt
new file mode 100644 (file)
index 0000000..ef3c95d
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.utils
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.JsonNodeFactory.instance
+import com.fasterxml.jackson.databind.node.ObjectNode
+
+fun jsonObject(block: ObjectNode.() -> Unit): ObjectNode = ObjectNode(instance).apply(block)
+
+fun jsonObject(vararg properties: Pair<String, Any?>) = jsonObject {
+       properties.forEach {
+               it.second.let { value ->
+                       when (value) {
+                               is String -> put(it.first, value)
+                               is Int -> put(it.first, value)
+                               is Long -> put(it.first, value)
+                               is Boolean -> put(it.first, value)
+                               else -> Unit
+                       }
+               }
+       }
+}
+
+fun jsonArray(vararg objects: String?): ArrayNode = objects.fold(ArrayNode(instance), ArrayNode::add)
+fun jsonArray(vararg objects: JsonNode?): ArrayNode = objects.fold(ArrayNode(instance), ArrayNode::add)
+
+fun Iterable<ObjectNode>.toArray(): ArrayNode = fold(ArrayNode(instance), ArrayNode::add)
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Objects.kt
new file mode 100644 (file)
index 0000000..36e530c
--- /dev/null
@@ -0,0 +1,3 @@
+package net.pterodactylus.sone.utils
+
+fun <T> T?.asList() = this?.let(::listOf) ?: emptyList<T>()
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Optionals.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Optionals.kt
new file mode 100644 (file)
index 0000000..326db9d
--- /dev/null
@@ -0,0 +1,11 @@
+package net.pterodactylus.sone.utils
+
+import com.google.common.base.Optional
+
+fun <T, R> Optional<T>.let(block: (T) -> R): R? = if (isPresent) block(get()) else null
+fun <T> Optional<T>.also(block: (T) -> Unit): Optional<T> { if (isPresent) block(get()); return this }
+
+fun <T> T?.asOptional(): Optional<T> = this?.let { Optional.of(it) } ?: Optional.absent<T>()
+
+fun <T, R> Iterable<T>.mapPresent(transform: (T) -> Optional<R>): List<R> =
+               map(transform).filter { it.isPresent }.map { it.get() }
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Pagination.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Pagination.kt
new file mode 100644 (file)
index 0000000..6c436e2
--- /dev/null
@@ -0,0 +1,44 @@
+package net.pterodactylus.sone.utils
+
+/**
+ * Helper class for lists that need pagination. Setting the page or the page
+ * size will automatically recalculate all other parameters, and the next call
+ * to [Pagination.items] retrieves all items on the current page.
+ * <p>
+ * A pagination object can be used as an [Iterable]. When the [Iterator]
+ * from [Pagination.iterator] is requested, the iterator over
+ * [Pagination.items] is returned.
+ *
+ * @param <T>
+ *            The type of the list elements
+ */
+class Pagination<out T>(private val originalItems: List<T>, pageSize: Int): Iterable<T> {
+
+       var page: Int = 0
+               set(value) {
+                       field = maxOf(0, minOf(value, lastPage))
+               }
+
+       var pageSize = pageSize
+               set(value) {
+                       val oldFirstIndex = page * field
+                       field = maxOf(1, value)
+                       page = oldFirstIndex / field
+               }
+
+       val pageNumber get() = page + 1
+       val pageCount get() = maxOf((originalItems.size - 1) / pageSize + 1, 1)
+       val itemCount get() = minOf(originalItems.size - page * pageSize, pageSize)
+       val items get() = originalItems.subList(page * pageSize, minOf(originalItems.size, (page + 1) * pageSize))
+       val isFirst get() = page == 0
+       val isLast get() = page == lastPage
+       val isNecessary get() = pageCount > 1
+       val previousPage get() = page - 1
+       val nextPage get() = page + 1
+       val lastPage get() = pageCount - 1
+
+       override fun iterator() = items.iterator()
+
+}
+
+fun <T> Iterable<T>.paginate(pageSize: Int) = Pagination<T>(toList(), pageSize)
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Requests.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Requests.kt
new file mode 100644 (file)
index 0000000..7b7957b
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.HTTPRequest
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import net.pterodactylus.util.web.Request
+
+val Request.isGET get() = this.method == GET
+val Request.isPOST get() = this.method == POST
+val HTTPRequest.isGET get() = method == "GET"
+val HTTPRequest.isPOST get() = method == "POST"
+
+val FreenetRequest.parameters get() = Parameters(httpRequest)
+val HTTPRequest.parameters get() = Parameters(this)
+
+class Parameters(private val request: HTTPRequest) {
+       operator fun get(name: String, maxLength: Int = 1048576) = when {
+               request.isGET -> request.getParam(name)
+               request.isPOST -> request.getPartAsStringFailsafe(name, maxLength)
+               else -> null
+       }
+
+       operator fun contains(name: String) = when {
+               request.isGET -> request.isParameterSet(name)
+               request.isPOST -> request.isPartSet(name)
+               else -> false
+       }
+}
+
+val FreenetRequest.headers get() = Headers(httpRequest)
+val HTTPRequest.headers get() = Headers(this)
+
+class Headers(private val request: HTTPRequest) {
+       operator fun get(name: String): String? = request.getHeader(name.toLowerCase())
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Strings.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Strings.kt
new file mode 100644 (file)
index 0000000..9390f98
--- /dev/null
@@ -0,0 +1,3 @@
+package net.pterodactylus.sone.utils
+
+val String?.emptyToNull get() = if ((this?.trim() ?: "") == "") null else this
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Templates.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Templates.kt
new file mode 100644 (file)
index 0000000..80920a5
--- /dev/null
@@ -0,0 +1,16 @@
+package net.pterodactylus.sone.utils
+
+import net.pterodactylus.util.template.Template
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateParser
+import java.io.StringReader
+import java.io.StringWriter
+
+fun String.asTemplate(): Template = StringReader(this).use { TemplateParser.parse(it) }
+
+fun Template.render(templateContext: TemplateContext) =
+               StringWriter().use {
+                       it.also {
+                               render(templateContext, it)
+                       }
+               }.toString()
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt b/src/main/kotlin/net/pterodactylus/sone/web/SessionProvider.kt
new file mode 100644 (file)
index 0000000..463ddaa
--- /dev/null
@@ -0,0 +1,14 @@
+package net.pterodactylus.sone.web
+
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+
+/**
+ * Provides access to the currently logged-in [Sone].
+ */
+interface SessionProvider {
+
+       fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true): Sone?
+       fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.kt
new file mode 100644 (file)
index 0000000..b185d2b
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user bookmark a post.
+ */
+class BookmarkAjaxPage(webInterface: WebInterface) : JsonPage("bookmark.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["post"].emptyToNull
+                                       ?.let(core::getPost)
+                                       ?.also(core::bookmarkPost)
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-post-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.kt
new file mode 100644 (file)
index 0000000..ec65b55
--- /dev/null
@@ -0,0 +1,32 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.headers
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX handler that creates a new post.
+ */
+class CreatePostAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("createPost.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["text"].emptyToNull
+                                       ?.let { TextFilter.filter(request.headers["Host"], it) }
+                                       ?.let { text ->
+                                               val sender = request.parameters["sender"].emptyToNull?.let(core::getSone)?.orNull() ?: currentSone
+                                               val recipient = request.parameters["recipient"].let(core::getSone)
+                                               core.createPost(sender, recipient, text).let { post ->
+                                                       createSuccessJsonObject().apply {
+                                                               put("postId", post.id)
+                                                               put("sone", sender.id)
+                                                               put("recipient", recipient.let(Sone::getId))
+                                                       }
+                                               }
+                                       } ?: createErrorJsonObject("text-required")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.kt
new file mode 100644 (file)
index 0000000..f817e28
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.headers
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * This AJAX page create a reply.
+ */
+class CreateReplyAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("createReply.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest): JsonReturnObject =
+                       request.parameters["post"].emptyToNull
+                                       ?.let(core::getPost)
+                                       ?.let { post ->
+                                               val text = TextFilter.filter(request.headers["Host"], request.parameters["text"])
+                                               val sender = request.parameters["sender"].let(core::getLocalSone) ?: currentSone
+                                               val reply = core.createReply(sender, post, text)
+                                               createSuccessJsonObject().apply {
+                                                       put("reply", reply.id)
+                                                       put("sone", sender.id)
+                                               }
+                                       }
+                                       ?: createErrorJsonObject("invalid-post-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.kt
new file mode 100644 (file)
index 0000000..76d867e
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.ifTrue
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * This AJAX page deletes a post.
+ */
+class DeletePostAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("deletePost.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["post"]
+                                       .let(core::getPost)
+                                       ?.let { post ->
+                                               post.sone.isLocal.ifTrue {
+                                                       createSuccessJsonObject().also {
+                                                               core.deletePost(post)
+                                                       }
+                                               } ?: createErrorJsonObject("not-authorized")
+                                       } ?: createErrorJsonObject("invalid-post-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.kt
new file mode 100644 (file)
index 0000000..05f53da
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user delete a profile field.
+ */
+class DeleteProfileFieldAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("deleteProfileField.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       currentSone.profile.let { profile ->
+                               request.parameters["field"]
+                                               ?.let(profile::getFieldById)
+                                               ?.let { field ->
+                                                       createSuccessJsonObject().also {
+                                                               profile.removeField(field)
+                                                               currentSone.profile = profile
+                                                               core.touchConfiguration()
+                                                       }
+                                               } ?: createErrorJsonObject("invalid-field-id")
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.kt
new file mode 100644 (file)
index 0000000..2108daf
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.ifTrue
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * This AJAX page deletes a reply.
+ */
+class DeleteReplyAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("deleteReply.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["reply"]
+                                       .let(core::getPostReply)
+                                       ?.let { reply ->
+                                               reply.sone.isLocal.ifTrue {
+                                                       createSuccessJsonObject().also {
+                                                               core.deleteReply(reply)
+                                                       }
+                                               } ?: createErrorJsonObject("not-authorized")
+                                       } ?: createErrorJsonObject("invalid-reply-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.kt
new file mode 100644 (file)
index 0000000..539d57b
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.ifTrue
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user dismiss a notification.
+ */
+class DismissNotificationAjaxPage(webInterface: WebInterface) : JsonPage("dismissNotification.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest): JsonReturnObject =
+                       request.parameters["notification"]!!
+                                       .let(webInterface::getNotification)
+                                       .let { notification ->
+                                               notification.isDismissable.ifTrue {
+                                                       createSuccessJsonObject().also {
+                                                               notification.dismiss()
+                                                       }
+                                               } ?: createErrorJsonObject("not-dismissable")
+                                       } ?: createErrorJsonObject("invalid-notification-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.kt
new file mode 100644 (file)
index 0000000..e4f8435
--- /dev/null
@@ -0,0 +1,28 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user distrust a Sone.
+ *
+ * @see Core.distrustSone(Sone, Sone)
+ */
+class DistrustAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("distrustSone.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       .let(core::getSone)
+                                       ?.let { sone ->
+                                               createSuccessJsonObject()
+                                                               .put("trustValue", core.preferences.negativeTrust)
+                                                               .also {
+                                                                       core.distrustSone(currentSone, sone)
+                                                               }
+                                       } ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.kt
new file mode 100644 (file)
index 0000000..321ee7d
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.headers
+import net.pterodactylus.sone.utils.ifTrue
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Page that stores a user’s album modifications.
+ */
+class EditAlbumAjaxPage(webInterface: WebInterface) : JsonPage("editAlbum.ajax", webInterface) {
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["album"]!!
+                                       .let(core::getAlbum)
+                                       ?.let { album ->
+                                               album.sone.isLocal.ifTrue {
+                                                       when {
+                                                               request.parameters["moveLeft"] == "true" -> createSuccessJsonObject().apply {
+                                                                       put("sourceAlbumId", album.id)
+                                                                       put("destinationAlbumId", album.parent.moveAlbumUp(album).id)
+                                                               }
+                                                               request.parameters["moveRight"] == "true" -> createSuccessJsonObject().apply {
+                                                                       put("sourceAlbumId", album.id)
+                                                                       put("destinationAlbumId", album.parent.moveAlbumDown(album).id)
+                                                               }
+                                                               else -> try {
+                                                                       album.modify()
+                                                                                       .setTitle(request.parameters["title"])
+                                                                                       .setDescription(TextFilter.filter(request.headers["Host"], request.parameters["description"]))
+                                                                                       .update()
+                                                                       createSuccessJsonObject()
+                                                                                       .put("albumId", album.id)
+                                                                                       .put("title", album.title)
+                                                                                       .put("description", album.description)
+                                                               } catch (e: IllegalStateException) {
+                                                                       createErrorJsonObject("invalid-album-title")
+                                                               }
+                                                       }
+                                               } ?: createErrorJsonObject("not-authorized")
+                                       } ?: createErrorJsonObject("invalid-album-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.kt
new file mode 100644 (file)
index 0000000..dbeb2d2
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.template.ParserFilter
+import net.pterodactylus.sone.template.RenderFilter
+import net.pterodactylus.sone.template.ShortenFilter
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.headers
+import net.pterodactylus.sone.utils.ifTrue
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Page that stores a user’s image modifications.
+ */
+class EditImageAjaxPage(webInterface: WebInterface,
+               private val parserFilter: ParserFilter,
+               private val shortenFilter: ShortenFilter,
+               private val renderFilter: RenderFilter) : JsonPage("editImage.ajax", webInterface) {
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["image"]
+                                       .let(core::getImage)
+                                       ?.let { image ->
+                                               image.sone.isLocal.ifTrue {
+                                                       when {
+                                                               request.parameters["moveLeft"] == "true" -> createSuccessJsonObject().apply {
+                                                                       put("sourceImageId", image.id)
+                                                                       put("destinationImageId", image.album.moveImageUp(image).id)
+                                                                       core.touchConfiguration()
+                                                               }
+                                                               request.parameters["moveRight"] == "true" -> createSuccessJsonObject().apply {
+                                                                       put("sourceImageId", image.id)
+                                                                       put("destinationImageId", image.album.moveImageDown(image).id)
+                                                                       core.touchConfiguration()
+                                                               }
+                                                               else -> request.parameters["title"]!!.let { title ->
+                                                                       title.trim().isNotBlank().ifTrue {
+                                                                               request.parameters["description"]!!.let { description ->
+                                                                                       image.modify()
+                                                                                                       .setTitle(title)
+                                                                                                       .setDescription(TextFilter.filter(request.headers["Host"], description))
+                                                                                                       .update().let { newImage ->
+                                                                                               createSuccessJsonObject().apply {
+                                                                                                       put("title", newImage.title)
+                                                                                                       put("description", newImage.description)
+                                                                                                       put("parsedDescription", newImage.description.let {
+                                                                                                               parserFilter.format(TemplateContext(), it, mutableMapOf("sone" to image.sone)).let {
+                                                                                                                       shortenFilter.format(TemplateContext(), it, mutableMapOf()).let {
+                                                                                                                               renderFilter.format(TemplateContext(), it, mutableMapOf()) as String
+                                                                                                                       }
+                                                                                                               }
+                                                                                                       })
+                                                                                                       core.touchConfiguration()
+                                                                                               }
+                                                                                       }
+                                                                               }
+                                                                       } ?: createErrorJsonObject("invalid-image-title")
+                                                               }
+                                                       }
+                                               } ?: createErrorJsonObject("not-authorized")
+                                       } ?: createErrorJsonObject("invalid-image-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.kt
new file mode 100644 (file)
index 0000000..e48c92d
--- /dev/null
@@ -0,0 +1,34 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.ifFalse
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user rename a profile field.
+ */
+class EditProfileFieldAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("editProfileField.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       currentSone.profile.let { profile ->
+                               request.parameters["field"]!!
+                                               .let(profile::getFieldById)
+                                               ?.let { field ->
+                                                       request.parameters["name"]!!.trim().let { newName ->
+                                                               newName.isBlank().ifFalse {
+                                                                       try {
+                                                                               field.name = newName
+                                                                               createSuccessJsonObject().also {
+                                                                                       currentSone.profile = profile
+                                                                               }
+                                                                       } catch (_: IllegalArgumentException) {
+                                                                               createErrorJsonObject("duplicate-field-name")
+                                                                       }
+                                                               }
+                                                       } ?: createErrorJsonObject("invalid-parameter-name")
+                                               } ?: createErrorJsonObject("invalid-field-id")
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.kt
new file mode 100644 (file)
index 0000000..c6aa25e
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets a Sone follow another Sone.
+ */
+class FollowSoneAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("followSone.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       .let(core::getSone)
+                                       ?.also { core.followSone(currentSone, it.id) }
+                                       ?.also(core::markSoneKnown)
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.kt
new file mode 100644 (file)
index 0000000..335f269
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.template.SoneAccessor
+import net.pterodactylus.sone.utils.jsonArray
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that retrieves the number of “likes” a [net.pterodactylus.sone.data.Post]
+ * or [net.pterodactylus.sone.data.PostReply] has.
+ */
+class GetLikesAjaxPage(webInterface: WebInterface) : JsonPage("getLikes.ajax", webInterface) {
+
+       override val needsFormPassword = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       when (request.parameters["type"]) {
+                               "post" -> request.parameters["post"]
+                                               .let(core::getPost)
+                                               ?.let(core::getLikes)
+                                               ?.toReply()
+                                               ?: createErrorJsonObject("invalid-post-id")
+                               "reply" -> request.parameters["reply"]
+                                               .let(core::getPostReply)
+                                               ?.let(core::getLikes)
+                                               ?.toReply()
+                                               ?: createErrorJsonObject("invalid-reply-id")
+                               else -> createErrorJsonObject("invalid-type")
+                       }
+
+       private fun Set<Sone>.toReply() = createSuccessJsonObject().apply {
+               put("likes", size)
+               put("sones", sortedBy { SoneAccessor.getNiceName(it) }
+                               .map {
+                                       jsonObject("id" to it.id, "name" to SoneAccessor.getNiceName(it))
+                               }
+                               .let { jsonArray(*it.toTypedArray()) }
+               )
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPage.kt
new file mode 100644 (file)
index 0000000..809abdc
--- /dev/null
@@ -0,0 +1,48 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.template.LinkedElementRenderFilter
+import net.pterodactylus.sone.utils.jsonArray
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Renders linked elements after they have been loaded.
+ */
+class GetLinkedElementAjaxPage(webInterface: WebInterface, private val elementLoader: ElementLoader, private val linkedElementRenderFilter: LinkedElementRenderFilter):
+               JsonPage("getLinkedElement.ajax", webInterface) {
+
+       override val needsFormPassword = false
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest): JsonReturnObject =
+                       request.httpRequest.getParam("elements", "[]").asJson()
+                                       .map(JsonNode::asText)
+                                       .map(elementLoader::loadElement)
+                                       .filterNot { it.loading }
+                                       .map { it to renderLinkedElement(it) }
+                                       .let { elements ->
+                                               jsonArray(
+                                                               *(elements.map { element ->
+                                                                       jsonObject {
+                                                                               put("link", element.first.link)
+                                                                               put("html", element.second)
+                                                                       }
+                                                               }.toTypedArray())
+                                               )
+                                       }.let { linkedElements ->
+                               createSuccessJsonObject().apply {
+                                       put("linkedElements", linkedElements)
+                               }
+                       }
+
+       private fun String.asJson() = ObjectMapper().readTree(this).asIterable()
+
+       private fun renderLinkedElement(linkedElement: LinkedElement) =
+                       linkedElementRenderFilter.format(null, linkedElement, emptyMap())
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt
new file mode 100644 (file)
index 0000000..19cdd24
--- /dev/null
@@ -0,0 +1,76 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.SoneOptions
+import net.pterodactylus.sone.main.SonePlugin
+import net.pterodactylus.sone.utils.jsonArray
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.notify.TemplateNotification
+import java.io.StringWriter
+
+/**
+ * AJAX handler to return all current notifications.
+ */
+class GetNotificationsAjaxPage(webInterface: WebInterface) : JsonPage("getNotifications.ajax", webInterface) {
+
+       override val needsFormPassword = false
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       getCurrentSone(request.toadletContext, false).let { currentSone ->
+                               webInterface.getNotifications(currentSone)
+                                               .sortedBy(Notification::getCreatedTime)
+                                               .let { notifications ->
+                                                       createSuccessJsonObject().apply {
+                                                               put("notificationHash", notifications.hashCode())
+                                                               put("options", currentSone?.options.asJsonObject)
+                                                               put("notifications", notifications.asJsonObject(currentSone, request))
+                                                       }
+                                               }
+                       }
+
+       private fun Collection<Notification>.asJsonObject(currentSone: Sone?, freenetRequest: FreenetRequest) = jsonArray(
+                       *map { notification ->
+                               jsonObject(
+                                               "id" to notification.id,
+                                               "createdTime" to notification.createdTime,
+                                               "lastUpdatedTime" to notification.lastUpdatedTime,
+                                               "dismissable" to notification.isDismissable,
+                                               "text" to if (notification is TemplateNotification) notification.render(currentSone, freenetRequest) else notification.render()
+                               )
+                       }.toTypedArray()
+       )
+
+       private fun TemplateNotification.render(currentSone: Sone?, freenetRequest: FreenetRequest) = StringWriter().use {
+               val mergedTemplateContext = webInterface.templateContextFactory.createTemplateContext()
+                               .mergeContext(templateContext)
+                               .apply {
+                                       this["core"] = core
+                                       this["currentSone"] = currentSone
+                                       this["localSones"] = core.localSones
+                                       this["request"] = freenetRequest
+                                       this["currentVersion"] = SonePlugin.getPluginVersion()
+                                       this["hasLatestVersion"] = core.updateChecker.hasLatestVersion()
+                                       this["latestEdition"] = core.updateChecker.latestEdition
+                                       this["latestVersion"] = core.updateChecker.latestVersion
+                                       this["latestVersionTime"] = core.updateChecker.latestVersionDate
+                                       this["notification"] = this@render
+                               }
+               it.also { render(mergedTemplateContext, it) }
+       }.toString()
+
+}
+
+private val SoneOptions?.asJsonObject
+       get() = this?.let { options ->
+               jsonObject(
+                               "ShowNotification/NewSones" to options.isShowNewSoneNotifications,
+                               "ShowNotification/NewPosts" to options.isShowNewPostNotifications,
+                               "ShowNotification/NewReplies" to options.isShowNewReplyNotifications
+               )
+       } ?: jsonObject {}
+
+private fun Notification.render() = StringWriter().use { it.also { render(it) } }.toString()
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.kt
new file mode 100644 (file)
index 0000000..2d902a6
--- /dev/null
@@ -0,0 +1,43 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.utils.render
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+
+/**
+ * This AJAX handler retrieves information and rendered representation of a [Post].
+ */
+class GetPostAjaxPage(webInterface: WebInterface, private val postTemplate: Template) : LoggedInJsonPage("getPost.ajax", webInterface) {
+
+       override val needsFormPassword = false
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["post"]
+                                       .let(core::getPost)
+                                       .let { post ->
+                                               createSuccessJsonObject().
+                                                               put("post", jsonObject(
+                                                                               "id" to post.id,
+                                                                               "sone" to post.sone.id,
+                                                                               "time" to post.time,
+                                                                               "recipient" to post.recipientId.orNull(),
+                                                                               "html" to post.render(currentSone, request)
+                                                               ))
+                                       } ?: createErrorJsonObject("invalid-post-id")
+
+       private fun Post.render(currentSone: Sone, request: FreenetRequest) =
+                       webInterface.templateContextFactory.createTemplateContext().apply {
+                               set("core", core)
+                               set("request", request)
+                               set("post", this@render)
+                               set("currentSone", currentSone)
+                               set("localSones", core.localSones)
+                       }.let { postTemplate.render(it) }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.kt
new file mode 100644 (file)
index 0000000..fc4b482
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.utils.render
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.Template
+
+/**
+ * This AJAX page returns the details of a reply.
+ */
+class GetReplyAjaxPage(webInterface: WebInterface, private val template: Template) : LoggedInJsonPage("getReply.ajax", webInterface) {
+
+       override val needsFormPassword = false
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["reply"]
+                                       .let(core::getPostReply)
+                                       ?.let { it.toJson(currentSone, request) }
+                                       ?.let { replyJson ->
+                                               createSuccessJsonObject().apply {
+                                                       put("reply", replyJson)
+                                               }
+                                       } ?: createErrorJsonObject("invalid-reply-id")
+
+       private fun PostReply.toJson(currentSone: Sone, request: FreenetRequest) = jsonObject(*mapOf(
+                       "id" to id,
+                       "soneId" to sone.id,
+                       "postId" to postId,
+                       "time" to time,
+                       "html" to render(currentSone, request)
+       ).toList().toTypedArray())
+
+       private fun PostReply.render(currentSone: Sone, request: FreenetRequest) =
+                       webInterface.templateContextFactory.createTemplateContext().apply {
+                               set("core", core)
+                               set("request", request)
+                               set("reply", this@render)
+                               set("currentSone", currentSone)
+                       }.let { template.render(it) }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.kt
new file mode 100644 (file)
index 0000000..acb8cec
--- /dev/null
@@ -0,0 +1,103 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.SoneOptions
+import net.pterodactylus.sone.freenet.L10nFilter
+import net.pterodactylus.sone.template.SoneAccessor
+import net.pterodactylus.sone.text.TimeTextConverter
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.mapPresent
+import net.pterodactylus.sone.utils.toArray
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import java.text.SimpleDateFormat
+import java.util.TimeZone
+
+/**
+ * The “get status” AJAX handler returns all information that is necessary to
+ * update the web interface in real-time.
+ */
+class GetStatusAjaxPage(webInterface: WebInterface, private val elementLoader: ElementLoader, private val timeTextConverter: TimeTextConverter, private val l10nFilter: L10nFilter, timeZone: TimeZone = TimeZone.getDefault()):
+               JsonPage("getStatus.ajax", webInterface) {
+
+       private val dateFormatter = SimpleDateFormat("MMM d, yyyy, HH:mm:ss").apply {
+               this.timeZone = timeZone
+       }
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       getCurrentSone(request.toadletContext, false).let { currentSone ->
+                               createSuccessJsonObject().apply {
+                                       this["loggedIn"] = currentSone != null
+                                       this["options"] = currentSone?.options?.toJsonOptions() ?: jsonObject {}
+                                       this["notificationHash"] = webInterface.getNotifications(currentSone).sortedBy { it.createdTime }.hashCode()
+                                       this["sones"] = request.httpRequest.getParam("soneIds").split(',').mapPresent(core::getSone).plus(currentSone).filterNotNull().toJsonSones()
+                                       this["newPosts"] = webInterface.getNewPosts(currentSone).toJsonPosts()
+                                       this["newReplies"] = webInterface.getNewReplies(currentSone).toJsonReplies()
+                                       this["linkedElements"] = request.httpRequest.getParam("elements", "[]").asJson().map(JsonNode::asText).map(elementLoader::loadElement).toJsonElements()
+                               }
+                       }
+
+       private operator fun JsonReturnObject.set(key: String, value: JsonNode) = put(key, value)
+       private operator fun JsonReturnObject.set(key: String, value: Int) = put(key, value)
+       private operator fun JsonReturnObject.set(key: String, value: Boolean) = put(key, value)
+
+       private fun String.asJson() = ObjectMapper().readTree(this).asIterable()
+
+       override val needsFormPassword = false
+       override val requiresLogin = false
+
+       private fun SoneOptions.toJsonOptions() = jsonObject {
+               put("ShowNotification/NewSones", isShowNewSoneNotifications)
+               put("ShowNotification/NewPosts", isShowNewPostNotifications)
+               put("ShowNotification/NewReplies", isShowNewReplyNotifications)
+       }
+
+       private fun Iterable<Sone>.toJsonSones() = map { sone ->
+               jsonObject {
+                       put("id", sone.id)
+                       put("name", SoneAccessor.getNiceName(sone))
+                       put("local", sone.isLocal)
+                       put("status", sone.status.name)
+                       put("modified", core.isModifiedSone(sone))
+                       put("locked", core.isLocked(sone))
+                       put("lastUpdatedUnknown", sone.time == 0L)
+                       synchronized(dateFormatter) {
+                               put("lastUpdated", dateFormatter.format(sone.time))
+                       }
+                       put("lastUpdatedText", timeTextConverter.getTimeText(sone.time).l10nText.let { l10nFilter.format(null, it, emptyMap()) })
+               }
+       }.toArray()
+
+       private fun Iterable<Post>.toJsonPosts() = map { post ->
+               jsonObject {
+                       put("id", post.id)
+                       put("sone", post.sone.id)
+                       put("time", post.time)
+                       put("recipient", post.recipientId.orNull())
+               }
+       }.toArray()
+
+       private fun Iterable<PostReply>.toJsonReplies() = map { reply ->
+               jsonObject {
+                       put("id", reply.id)
+                       put("sone", reply.sone.id)
+                       put("post", reply.postId)
+                       put("postSone", reply.post.get().sone.id)
+               }
+       }.toArray()
+
+       private fun Iterable<LinkedElement>.toJsonElements() = map { (link, failed, loading) ->
+               jsonObject {
+                       put("link", link)
+                       put("loading", loading)
+                       put("failed", failed)
+               }
+       }.toArray()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.kt
new file mode 100644 (file)
index 0000000..fb2592e
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.freenet.L10nFilter
+import net.pterodactylus.sone.text.TimeTextConverter
+import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import java.text.SimpleDateFormat
+import java.util.TimeZone
+
+/**
+ * Ajax page that returns a formatted, relative timestamp for replies or posts.
+ */
+class GetTimesAjaxPage(webInterface: WebInterface,
+               private val timeTextConverter: TimeTextConverter,
+               private val l10nFilter: L10nFilter,
+               timeZone: TimeZone) : JsonPage("getTimes.ajax", webInterface) {
+
+       private val dateTimeFormatter = SimpleDateFormat("MMM d, yyyy, HH:mm:ss").apply {
+               this.timeZone = timeZone
+       }
+
+       override val needsFormPassword = false
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       createSuccessJsonObject().apply {
+                               put("postTimes", request.parameters["posts"]!!.idsToJson { core.getPost(it)?.let { it.id to it.time } })
+                               put("replyTimes", request.parameters["replies"]!!.idsToJson { core.getPostReply(it)?.let { it.id to it.time } })
+                       }
+
+       private fun String.idsToJson(transform: (String) -> Pair<String, Long>?) =
+                       split(",").mapNotNull(transform).toJson()
+
+       private fun List<Pair<String, Long>>.toJson() = jsonObject {
+               this@toJson.map { (id, time) ->
+                       val timeText = timeTextConverter.getTimeText(time)
+                       id to jsonObject(
+                                       "timeText" to l10nFilter.format(null, timeText.l10nText, emptyMap()),
+                                       "refreshTime" to timeText.refreshTime / 1000,
+                                       "tooltip" to synchronized(dateTimeFormatter) {
+                                               dateTimeFormatter.format(time)
+                                       })
+               }.forEach { this@jsonObject.set(it.first, it.second) }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPage.kt
new file mode 100644 (file)
index 0000000..d260655
--- /dev/null
@@ -0,0 +1,19 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Returns the translation for a given key as JSON object.
+ */
+class GetTranslationAjaxPage(webInterface: WebInterface) : JsonPage("getTranslation.ajax", webInterface) {
+
+       override val needsFormPassword = false
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       createSuccessJsonObject()
+                                       .put("value", webInterface.l10n.getString(request.parameters["key"]))
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObject.kt
new file mode 100644 (file)
index 0000000..535a3d0
--- /dev/null
@@ -0,0 +1,6 @@
+package net.pterodactylus.sone.web.ajax
+
+/**
+ * [JsonReturnObject] that signals an error has occured.
+ */
+data class JsonErrorReturnObject(val error: String) : JsonReturnObject(false)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonPage.kt
new file mode 100644 (file)
index 0000000..2da5edc
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.SessionProvider
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.web.Page
+import net.pterodactylus.util.web.Response
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+/**
+ * A JSON page is a specialized [Page] that will always return a JSON
+ * object to the browser, e.g. for use with AJAX or other scripting frameworks.
+ */
+abstract class JsonPage(private val path: String, protected val webInterface: WebInterface) : Page<FreenetRequest> {
+
+       private val objectMapper = ObjectMapper()
+       private val sessionProvider: SessionProvider = webInterface
+       protected val core = webInterface.core
+
+       override fun getPath() = path
+       override fun isPrefixPage() = false
+
+       open val needsFormPassword = true
+       open val requiresLogin = true
+
+       protected fun createSuccessJsonObject() = JsonReturnObject(true)
+       protected fun createErrorJsonObject(error: String) =
+                       JsonErrorReturnObject(error)
+
+       protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) =
+                       sessionProvider.getCurrentSone(toadletContext, createSession)
+
+       override fun handleRequest(request: FreenetRequest, response: Response): Response {
+               if (core.preferences.isRequireFullAccess && !request.toadletContext.isAllowedFullAccess) {
+                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(createErrorJsonObject("auth-required").asJsonString())
+               }
+               if (needsFormPassword && request.parameters["formPassword"] != webInterface.formPassword) {
+                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(createErrorJsonObject("auth-required").asJsonString())
+               }
+               if (requiresLogin && (sessionProvider.getCurrentSone(request.toadletContext, false) == null)) {
+                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(createErrorJsonObject("auth-required").asJsonString())
+               }
+               return try {
+                       response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(createJsonObject(request).asJsonString())
+               } catch (e: Exception) {
+                       response.setStatusCode(500).setStatusText(e.message).setContentType("text/plain").write(e.dumpStackTrace())
+               }
+       }
+
+       abstract fun createJsonObject(request: FreenetRequest): JsonReturnObject
+
+       private fun JsonReturnObject.asJsonString(): String = objectMapper.writeValueAsString(this)
+
+       private fun Throwable.dumpStackTrace(): String = ByteArrayOutputStream().use {
+               PrintStream(it, true, "UTF-8").use {
+                       this.printStackTrace(it)
+               }
+               it.toString("UTF-8")
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObject.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObject.kt
new file mode 100644 (file)
index 0000000..3d18d05
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.BooleanNode
+import com.fasterxml.jackson.databind.node.IntNode
+import com.fasterxml.jackson.databind.node.TextNode
+
+/**
+ * JSON return object for AJAX requests.
+ */
+open class JsonReturnObject(val isSuccess: Boolean) {
+
+       private val values = mutableMapOf<String, JsonNode?>()
+
+       val content: Map<String, Any?>
+               @JsonAnyGetter get() = values
+
+       operator fun get(key: String) = values[key]
+
+       fun put(key: String, value: String?) = apply {
+               values[key] = TextNode.valueOf(value)
+       }
+
+       fun put(key: String, value: Int) = apply {
+               values[key] = IntNode.valueOf(value)
+       }
+
+       fun put(key: String, value: Boolean) = apply {
+               values[key] = BooleanNode.valueOf(value)
+       }
+
+       fun put(key: String, value: JsonNode) = apply {
+               values[key] = value
+       }
+
+       override fun hashCode(): Int {
+               return isSuccess.hashCode() xor content.hashCode()
+       }
+
+       override fun equals(other: Any?) =
+                       (other as? JsonReturnObject)?.let {
+                               it.isSuccess == isSuccess && it.content == content
+                       } ?: false
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPage.kt
new file mode 100644 (file)
index 0000000..4257725
--- /dev/null
@@ -0,0 +1,32 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user like a [net.pterodactylus.sone.data.Post].
+ */
+class LikeAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("like.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       when (request.parameters["type"]) {
+                               "post" -> request.parameters["post"]
+                                               .let(core::getPost)
+                                               ?.let { currentSone.addLikedPostId(it.id) }
+                                               ?.also { core.touchConfiguration() }
+                                               ?.let { createSuccessJsonObject() }
+                                               ?: createErrorJsonObject("invalid-post-id")
+                               "reply" -> request.parameters["reply"]
+                                               .let(core::getPostReply)
+                                               ?.let { currentSone.addLikedReplyId(it.id) }
+                                               ?.also { core.touchConfiguration() }
+                                               ?.let { createSuccessJsonObject() }
+                                               ?: createErrorJsonObject("invalid-reply-id")
+                               else -> createErrorJsonObject("invalid-type")
+                       }
+
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.kt
new file mode 100644 (file)
index 0000000..8605fe6
--- /dev/null
@@ -0,0 +1,21 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Lets the user [lock][net.pterodactylus.sone.core.Core.lockSone] a [Sone][net.pterodactylus.sone.data.Sone].
+ */
+class LockSoneAjaxPage(webInterface: WebInterface) : JsonPage("lockSone.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       .let(core::getLocalSone)
+                                       ?.let(core::lockSone)
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPage.kt
new file mode 100644 (file)
index 0000000..388c289
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Base JSON page for all pages that require the user to be logged in.
+ */
+open class LoggedInJsonPage(path: String, webInterface: WebInterface) : JsonPage(path, webInterface) {
+
+       final override val requiresLogin = true
+
+       final override fun createJsonObject(request: FreenetRequest) =
+                       createJsonObject(getCurrentSone(request.toadletContext)!!, request)
+
+       open protected fun createJsonObject(currentSone: Sone, request: FreenetRequest): JsonReturnObject =
+                       createErrorJsonObject("not-implemented")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.kt
new file mode 100644 (file)
index 0000000..9e4e4a3
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.google.common.base.Optional
+import net.pterodactylus.sone.utils.mapPresent
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user mark a number of [net.pterodactylus.sone.data.Sone]s,
+ * [net.pterodactylus.sone.data.Post]s, or [net.pterodactylus.sone.data.Reply]s as known.
+ */
+class MarkAsKnownAjaxPage(webInterface: WebInterface) : JsonPage("markAsKnown.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) = when (request.parameters["type"]) {
+               "sone" -> processIds(request, core::getSone, core::markSoneKnown)
+               "post" -> processIds(request, core::getPost, core::markPostKnown)
+               "reply" -> processIds(request, core::getPostReply, core::markReplyKnown)
+               else -> createErrorJsonObject("invalid-type")
+       }
+
+       private fun <T> processIds(request: FreenetRequest, getter: (String) -> Optional<T>, marker: (T) -> Unit) =
+                       request.parameters["id"]
+                                       ?.split(Regex(" +"))
+                                       ?.mapPresent(getter)
+                                       ?.onEach(marker)
+                                       .let { createSuccessJsonObject() }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.kt
new file mode 100644 (file)
index 0000000..1c050bf
--- /dev/null
@@ -0,0 +1,41 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Profile.Field
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user move a profile field up or down.
+ *
+ * @see net.pterodactylus.sone.data.Profile#moveFieldUp(Field)
+ * @see net.pterodactylus.sone.data.Profile#moveFieldDown(Field)
+ */
+class MoveProfileFieldAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("moveProfileField.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       currentSone.profile.let { profile ->
+                               request.parameters["field"]
+                                               ?.let(profile::getFieldById)
+                                               ?.let { processField(currentSone, profile, it, request.parameters["direction"]) }
+                                               ?: createErrorJsonObject("invalid-field-id")
+                       }
+
+       private fun processField(currentSone: Sone, profile: Profile, field: Field, direction: String?) =
+                       try {
+                               when (direction) {
+                                       "up" -> profile.moveFieldUp(field)
+                                       "down" -> profile.moveFieldDown(field)
+                                       else -> null
+                               }?.let {
+                                       currentSone.profile = profile
+                                       core.touchConfiguration()
+                                       createSuccessJsonObject()
+                               } ?: createErrorJsonObject("invalid-direction")
+                       } catch (e: IllegalArgumentException) {
+                               createErrorJsonObject("not-possible")
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPage.kt
new file mode 100644 (file)
index 0000000..9a01af8
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user trust a Sone.
+ *
+ * @see net.pterodactylus.sone.core.Core.trustSone
+ */
+class TrustAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("trustSone.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       .let(core::getSone)
+                                       ?.let { core.trustSone(currentSone, it) }
+                                       ?.let { createSuccessJsonObject().put("trustValue", core.preferences.positiveTrust) }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.kt
new file mode 100644 (file)
index 0000000..f8331fe
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user unbookmark a post.
+ */
+class UnbookmarkAjaxPage(webInterface: WebInterface) : JsonPage("unbookmark.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["post"]
+                                       ?.let(core::getPost)
+                                       ?.also(core::unbookmarkPost)
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-post-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.kt
new file mode 100644 (file)
index 0000000..6889d04
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets a Sone unfollow another Sone.
+ */
+class UnfollowSoneAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("unfollowSone.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       ?.takeIf { core.getSone(it).isPresent }
+                                       ?.also { core.unfollowSone(currentSone, it) }
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.kt
new file mode 100644 (file)
index 0000000..b30829a
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user unlike a [net.pterodactylus.sone.data.Post].
+ */
+class UnlikeAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("unlike.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) = when (request.parameters["type"]) {
+               "post" -> request.processEntity("post", currentSone::removeLikedPostId)
+               "reply" -> request.processEntity("reply", currentSone::removeLikedReplyId)
+               else -> createErrorJsonObject("invalid-type")
+       }
+
+       private fun FreenetRequest.processEntity(entity: String, likeRemover: (String) -> Unit) =
+                       parameters[entity].emptyToNull
+                                       ?.also(likeRemover)
+                                       ?.also { core.touchConfiguration() }
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-$entity-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.kt
new file mode 100644 (file)
index 0000000..7d77c05
--- /dev/null
@@ -0,0 +1,22 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * Lets the user [unlock][net.pterodactylus.sone.core.Core.unlockSone] a [Sone][net.pterodactylus.sone.data.Sone].
+ */
+class UnlockSoneAjaxPage(webInterface: WebInterface) : JsonPage("unlockSone.ajax", webInterface) {
+
+       override val requiresLogin = false
+
+       override fun createJsonObject(request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       ?.let(core::getLocalSone)
+                                       ?.also(core::unlockSone)
+                                       ?.also { core.touchConfiguration() }
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.kt
new file mode 100644 (file)
index 0000000..c6274a2
--- /dev/null
@@ -0,0 +1,22 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+
+/**
+ * AJAX page that lets the user [untrust][net.pterodactylus.sone.core.Core.untrustSone] a [Sone].
+ */
+class UntrustAjaxPage(webInterface: WebInterface) : LoggedInJsonPage("untrustSone.ajax", webInterface) {
+
+       override fun createJsonObject(currentSone: Sone, request: FreenetRequest) =
+                       request.parameters["sone"]
+                                       ?.let(core::getSone)
+                                       ?.also { core.untrustSone(currentSone, it) }
+                                       ?.let { createSuccessJsonObject() }
+                                       ?: createErrorJsonObject("invalid-sone-id")
+
+}
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..d3382b4
--- /dev/null
@@ -0,0 +1,25 @@
+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.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(freenetRequest: 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..519d49e
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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..fbfc7d3
--- /dev/null
@@ -0,0 +1,24 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.utils.Pagination
+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 browse all his bookmarked posts.
+ */
+class BookmarksPage(template: Template, webInterface: WebInterface): SoneTemplatePage("bookmarks.html", template, "Page.Bookmarks.Title", webInterface) {
+
+       override fun handleRequest(freenetRequest: 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..ee3936e
--- /dev/null
@@ -0,0 +1,42 @@
+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.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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val name = freenetRequest.httpRequest.getPartAsStringFailsafe("name", 64).trim()
+                       if (name.isEmpty()) {
+                               templateContext["nameMissing"] = true
+                               return
+                       }
+                       val description = freenetRequest.httpRequest.getPartAsStringFailsafe("description", 256).trim()
+                       val currentSone = webInterface.getCurrentSoneCreatingSession(freenetRequest.toadletContext)
+                       val parentId = freenetRequest.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(freenetRequest.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..30a1c31
--- /dev/null
@@ -0,0 +1,32 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               templateContext["returnPage"] = returnPage
+               if (freenetRequest.isPOST) {
+                       val text = freenetRequest.httpRequest.getPartAsStringFailsafe("text", 65536).trim()
+                       if (text == "") {
+                               templateContext["errorTextEmpty"] = true
+                               return
+                       }
+                       val sender = webInterface.core.getLocalSone(freenetRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(freenetRequest.toadletContext)
+                       val recipient = webInterface.core.getSone(freenetRequest.httpRequest.getPartAsStringFailsafe("recipient", 43))
+                       webInterface.core.createPost(sender, recipient, TextFilter.filter(freenetRequest.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..a6c58e4
--- /dev/null
@@ -0,0 +1,32 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val postId = freenetRequest.httpRequest.getPartAsStringFailsafe("post", 36).apply { templateContext["postId"] = this }
+               val text = freenetRequest.httpRequest.getPartAsStringFailsafe("text", 65536).trim().apply { templateContext["text"] = this }
+               val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256).apply { templateContext["returnPage"] = this }
+               if (freenetRequest.isPOST) {
+                       if (text == "") {
+                               templateContext["errorTextEmpty"] = true
+                               return
+                       }
+                       val post = webInterface.core.getPost(postId).orNull() ?: throw RedirectException("noPermission.html")
+                       val sender = webInterface.core.getLocalSone(freenetRequest.httpRequest.getPartAsStringFailsafe("sender", 43)) ?: getCurrentSone(freenetRequest.toadletContext)
+                       webInterface.core.createReply(sender, post, TextFilter.filter(freenetRequest.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..efa6f1d
--- /dev/null
@@ -0,0 +1,45 @@
+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.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(freenetRequest: 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 (freenetRequest.isPOST) {
+                       val identity = freenetRequest.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(freenetRequest.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..98eea86
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val album = webInterface.core.getAlbum(freenetRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
+                       if (!album.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (freenetRequest.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}")
+               }
+               val album = webInterface.core.getAlbum(freenetRequest.httpRequest.getParam("album"))
+               templateContext["album"] = album ?: throw RedirectException("invalid.html")
+       }
+
+}
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..61db304
--- /dev/null
@@ -0,0 +1,34 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val image = webInterface.core.getImage(freenetRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
+                       if (!image.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (freenetRequest.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException("imageBrowser.html?image=${image.id}")
+                       }
+                       webInterface.core.deleteImage(image)
+                       throw RedirectException("imageBrowser.html?album=${image.album.id}")
+               }
+               val image = webInterface.core.getImage(freenetRequest.httpRequest.getParam("image")) ?: throw RedirectException("invalid.html")
+               if (!image.sone.isLocal) {
+                       throw RedirectException("noPermission.html")
+               }
+               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..21465d7
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val post = webInterface.core.getPost(freenetRequest.httpRequest.getPartAsStringFailsafe("post", 36)).orNull() ?: throw RedirectException("noPermission.html")
+                       val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+                       if (!post.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       if (freenetRequest.httpRequest.isPartSet("confirmDelete")) {
+                               webInterface.core.deletePost(post)
+                               throw RedirectException(returnPage)
+                       } else if (freenetRequest.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException(returnPage)
+                       }
+                       templateContext["post"] = post
+                       templateContext["returnPage"] = returnPage
+                       return
+               }
+               templateContext["post"] = webInterface.core.getPost(freenetRequest.httpRequest.getParam("post")).orNull() ?: throw RedirectException("noPermission.html")
+               templateContext["returnPage"] = freenetRequest.httpRequest.getParam("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..cf5ac6e
--- /dev/null
@@ -0,0 +1,28 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val currentSone = getCurrentSone(freenetRequest.toadletContext)!!
+               if (freenetRequest.isPOST) {
+                       val field = currentSone.profile.getFieldById(freenetRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
+                       if (freenetRequest.httpRequest.getPartAsStringFailsafe("confirm", 4) == "true") {
+                               currentSone.profile = currentSone.profile.apply { removeField(field) }
+                       }
+                       throw RedirectException("editProfile.html#profile-fields")
+               }
+               val field = currentSone.profile.getFieldById(freenetRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html")
+               templateContext["field"] = field
+       }
+
+}
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..c67d620
--- /dev/null
@@ -0,0 +1,38 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val replyId = freenetRequest.httpRequest.getPartAsStringFailsafe("reply", 36)
+                       val reply = webInterface.core.getPostReply(replyId).orNull() ?: throw RedirectException("noPermission.html")
+                       if (!reply.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+                       if (freenetRequest.httpRequest.isPartSet("confirmDelete")) {
+                               webInterface.core.deleteReply(reply)
+                               throw RedirectException(returnPage)
+                       }
+                       if (freenetRequest.httpRequest.isPartSet("abortDelete")) {
+                               throw RedirectException(returnPage)
+                       }
+                       templateContext["reply"] = replyId
+                       templateContext["returnPage"] = returnPage
+                       return
+               }
+               templateContext["reply"] = freenetRequest.httpRequest.getParam("reply")
+               templateContext["returnPage"] = freenetRequest.httpRequest.getParam("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..0cce5ba
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       if (freenetRequest.httpRequest.isPartSet("deleteSone")) {
+                               webInterface.core.deleteSone(getCurrentSone(freenetRequest.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..78cdb83
--- /dev/null
@@ -0,0 +1,21 @@
+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
+
+/**
+ * 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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val returnPage = freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256)
+               val notificationId = freenetRequest.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..ce5aaec
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val sone = webInterface.core.getSone(freenetRequest.httpRequest.getPartAsStringFailsafe("sone", 44)).orNull()
+                       sone?.run { webInterface.core.distrustSone(getCurrentSone(freenetRequest.toadletContext), this) }
+                       throw RedirectException(freenetRequest.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..bd612c3
--- /dev/null
@@ -0,0 +1,43 @@
+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.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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val album = webInterface.core.getAlbum(freenetRequest.httpRequest.getPartAsStringFailsafe("album", 36)) ?: throw RedirectException("invalid.html")
+                       album.takeUnless { it.sone.isLocal }?.run { throw RedirectException("noPermission.html") }
+                       if (freenetRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
+                               album.parent?.moveAlbumUp(album)
+                               webInterface.core.touchConfiguration()
+                               throw RedirectException("imageBrowser.html?album=${album.parent?.id}")
+                       } else if (freenetRequest.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(freenetRequest.httpRequest.getPartAsStringFailsafe("title", 100))
+                                                       .setDescription(freenetRequest.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/EditImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditImagePage.kt
new file mode 100644 (file)
index 0000000..b674cb9
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Image.Modifier.ImageTitleMustNotBeEmpty
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+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 title and description of an {@link Image}.
+ */
+class EditImagePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("editImage.html", template, "Page.EditImage.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val image = webInterface.core.getImage(freenetRequest.httpRequest.getPartAsStringFailsafe("image", 36)) ?: throw RedirectException("invalid.html")
+                       if (!image.sone.isLocal) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256).let { returnPage ->
+                               if (freenetRequest.httpRequest.getPartAsStringFailsafe("moveLeft", 4) == "true") {
+                                       image.album.moveImageUp(image)
+                                       webInterface.core.touchConfiguration()
+                               } else if (freenetRequest.httpRequest.getPartAsStringFailsafe("moveRight", 4) == "true") {
+                                       image.album.moveImageDown(image)
+                                       webInterface.core.touchConfiguration()
+                               } else {
+                                       try {
+                                               image.modify()
+                                                               .setTitle(freenetRequest.httpRequest.getPartAsStringFailsafe("title", 100))
+                                                               .setDescription(TextFilter.filter(freenetRequest.httpRequest.getHeader("Host"), freenetRequest.httpRequest.getPartAsStringFailsafe("description", 1024)))
+                                                               .update()
+                                               webInterface.core.touchConfiguration()
+                                       } catch (e: ImageTitleMustNotBeEmpty) {
+                                               throw RedirectException("emptyImageTitle.html")
+                                       }
+                               }
+                               throw RedirectException(returnPage)
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfileFieldPage.kt
new file mode 100644 (file)
index 0000000..5e3d3c6
--- /dev/null
@@ -0,0 +1,41 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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 of a profile field.
+ */
+class EditProfileFieldPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               sessionProvider.getCurrentSone(freenetRequest.toadletContext)!!.let { currentSone ->
+                       currentSone.profile.let { profile ->
+                               if (freenetRequest.isPOST) {
+                                       if (freenetRequest.httpRequest.getPartAsStringFailsafe("cancel", 4) == "true") {
+                                               throw RedirectException("editProfile.html#profile-fields")
+                                       }
+                                       val field = profile.getFieldById(freenetRequest.httpRequest.getPartAsStringFailsafe("field", 36)) ?: throw RedirectException("invalid.html")
+                                       freenetRequest.httpRequest.getPartAsStringFailsafe("name", 256).let { name ->
+                                               try {
+                                                       if (name != field.name) {
+                                                               field.name = name
+                                                               currentSone.profile = profile
+                                                       }
+                                                       throw RedirectException("editProfile.html#profile-fields")
+                                               } catch (e: IllegalArgumentException) {
+                                                       templateContext["duplicateFieldName"] = true
+                                                       return
+                                               }
+                                       }
+                               }
+                               templateContext["field"] = profile.getFieldById(freenetRequest.httpRequest.getParam("field")) ?: throw RedirectException("invalid.html")
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/EditProfilePage.kt
new file mode 100644 (file)
index 0000000..5ed0c2f
--- /dev/null
@@ -0,0 +1,73 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile.DuplicateField
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.isPOST
+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 edit her profile.
+ */
+class EditProfilePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("editProfile.html", template, "Page.EditProfile.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               freenetRequest.currentSone!!.profile.let { profile ->
+                       templateContext["firstName"] = profile.firstName
+                       templateContext["middleName"] = profile.middleName
+                       templateContext["lastName"] = profile.lastName
+                       templateContext["birthDay"] = profile.birthDay
+                       templateContext["birthMonth"] = profile.birthMonth
+                       templateContext["birthYear"] = profile.birthYear
+                       templateContext["avatarId"] = profile.avatar
+                       templateContext["fields"] = profile.fields
+                       if (freenetRequest.isPOST) {
+                               if (freenetRequest.httpRequest.getPartAsStringFailsafe("save-profile", 4) == "true") {
+                                       profile.firstName = freenetRequest.httpRequest.getPartAsStringFailsafe("first-name", 256).trim()
+                                       profile.middleName = freenetRequest.httpRequest.getPartAsStringFailsafe("middle-name", 256).trim()
+                                       profile.lastName = freenetRequest.httpRequest.getPartAsStringFailsafe("last-name", 256).trim()
+                                       profile.birthDay = freenetRequest.httpRequest.getPartAsStringFailsafe("birth-day", 256).trim().toIntOrNull()
+                                       profile.birthMonth = freenetRequest.httpRequest.getPartAsStringFailsafe("birth-month", 256).trim().toIntOrNull()
+                                       profile.birthYear = freenetRequest.httpRequest.getPartAsStringFailsafe("birth-year", 256).trim().toIntOrNull()
+                                       profile.setAvatar(webInterface.core.getImage(freenetRequest.httpRequest.getPartAsStringFailsafe("avatarId", 256).trim(), false))
+                                       profile.fields.forEach { field ->
+                                               field.value = TextFilter.filter(freenetRequest.httpRequest.getHeader("Host"), freenetRequest.httpRequest.getPartAsStringFailsafe("field-${field.id}", 400).trim())
+                                       }
+                                       webInterface.core.touchConfiguration()
+                                       throw RedirectException("editProfile.html")
+                               } else if (freenetRequest.httpRequest.getPartAsStringFailsafe("add-field", 4) == "true") {
+                                       val fieldName = freenetRequest.httpRequest.getPartAsStringFailsafe("field-name", 100)
+                                       try {
+                                               profile.addField(fieldName)
+                                               freenetRequest.currentSone!!.profile = profile
+                                               webInterface.core.touchConfiguration()
+                                               throw RedirectException("editProfile.html#profile-fields")
+                                       } catch (e: DuplicateField) {
+                                               templateContext["fieldName"] = fieldName
+                                               templateContext["duplicateFieldName"] = true
+                                       }
+                               } else profile.fields.forEach { field ->
+                                       if (freenetRequest.httpRequest.getPartAsStringFailsafe("delete-field-${field.id}", 4) == "true") {
+                                               throw RedirectException("deleteProfileField.html?field=${field.id}")
+                                       } else if (freenetRequest.httpRequest.getPartAsStringFailsafe("edit-field-${field.id}", 4) == "true") {
+                                               throw RedirectException("editProfileField.html?field=${field.id}")
+                                       } else if (freenetRequest.httpRequest.getPartAsStringFailsafe("move-down-field-${field.id}", 4) == "true") {
+                                               profile.moveFieldDown(field)
+                                               freenetRequest.currentSone!!.profile = profile
+                                               throw RedirectException("editProfile.html#profile-fields")
+                                       } else if (freenetRequest.httpRequest.getPartAsStringFailsafe("move-up-field-${field.id}", 4) == "true") {
+                                               profile.moveFieldUp(field)
+                                               freenetRequest.currentSone!!.profile = profile
+                                               throw RedirectException("editProfile.html#profile-fields")
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private val FreenetRequest.currentSone get() = sessionProvider.getCurrentSone(toadletContext)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/FollowSonePage.kt
new file mode 100644 (file)
index 0000000..781e07f
--- /dev/null
@@ -0,0 +1,31 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+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 follow another Sone.
+ */
+class FollowSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("followSone.html", template, "Page.FollowSone.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       freenetRequest.httpRequest.getPartAsStringFailsafe("sone", 1200).split(Regex("[ ,]+"))
+                                       .map { it to webInterface.core.getSone(it) }
+                                       .filter { it.second.isPresent }
+                                       .map { it.first to it.second.get() }
+                                       .forEach { sone ->
+                                               webInterface.core.followSone(freenetRequest.currentSone, sone.first)
+                                               webInterface.core.markSoneKnown(sone.second)
+                                       }
+                       throw RedirectException(freenetRequest.httpRequest.getPartAsStringFailsafe("returnPage", 256))
+               }
+       }
+
+       private val FreenetRequest.currentSone get() = sessionProvider.getCurrentSone(toadletContext)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/GetImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/GetImagePage.kt
new file mode 100644 (file)
index 0000000..932827e
--- /dev/null
@@ -0,0 +1,42 @@
+package net.pterodactylus.sone.web.pages
+
+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
+import java.net.URI
+
+/**
+ * Page that delivers a {@link TemporaryImage} to the browser.
+ */
+class GetImagePage(webInterface: WebInterface): FreenetPage {
+
+       private val core = webInterface.core
+
+       override fun getPath(): String {
+               return "getImage.html"
+       }
+
+       override fun isPrefixPage(): Boolean {
+               return false
+       }
+
+       override fun handleRequest(request: FreenetRequest, response: Response): Response {
+               val image = core.getTemporaryImage(request.httpRequest.getParam("image")) ?: return response.apply {
+                       statusCode = 404
+                       statusText = "Not found."
+                       contentType = "text/html; charset=utf-8"
+               }
+               return response.apply {
+                       statusCode = 200
+                       contentType = image.mimeType
+                       content.write(image.imageData)
+                       addHeader("Content-Disposition", "attachment; filename=${image.id}.${image.mimeType.split('/')[1]}")
+               }
+       }
+
+       override fun isLinkExcepted(link: URI?): Boolean {
+               return false
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ImageBrowserPage.kt
new file mode 100644 (file)
index 0000000..3181355
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.parameters
+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.net.URI
+
+/**
+ * The image browser page is the entry page for the image management.
+ */
+class ImageBrowserPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("imageBrowser.html", template, "Page.ImageBrowser.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if ("album" in freenetRequest.parameters) {
+                       templateContext["albumRequested"] = true
+                       templateContext["album"] = webInterface.core.getAlbum(freenetRequest.parameters["album"]!!)
+                       templateContext["page"] = freenetRequest.parameters["page"]
+               } else if ("image" in freenetRequest.parameters) {
+                       templateContext["imageRequested"] = true
+                       templateContext["image"] = webInterface.core.getImage(freenetRequest.parameters["image"])
+               } else if (freenetRequest.parameters["mode"] == "gallery") {
+                       templateContext["galleryRequested"] = true
+                       webInterface.core.sones
+                                       .map(Sone::getRootAlbum)
+                                       .flatMap(Album::getAlbums)
+                                       .flatMap { Album.FLATTENER.apply(it)!! }
+                                       .filterNot(Album::isEmpty)
+                                       .sortedBy(Album::getTitle)
+                                       .also { albums ->
+                                               Pagination(albums, webInterface.core.preferences.imagesPerPage).apply { page = freenetRequest.parameters["page"]?.toIntOrNull() ?: 0 }.also { pagination ->
+                                                       templateContext["albumPagination"] = pagination
+                                                       templateContext["albums"] = pagination.items
+                                               }
+                                       }
+               } else {
+                       templateContext["soneRequested"] = true
+                       templateContext["sone"] = webInterface.core.getSone(freenetRequest.httpRequest.getParam("sone")).orNull() ?: getCurrentSone(freenetRequest.toadletContext)
+               }
+       }
+
+       override fun isLinkExcepted(link: URI?) = true
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/IndexPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/IndexPage.kt
new file mode 100644 (file)
index 0000000..bf6e0c4
--- /dev/null
@@ -0,0 +1,41 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.notify.PostVisibilityFilter
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.parameters
+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 index page shows the main page of Sone. This page will contain the posts
+ * of all friends of the current user.
+ */
+class IndexPage(template: Template, webInterface: WebInterface, private val postVisibilityFilter: PostVisibilityFilter):
+               SoneTemplatePage("index.html", template, "Page.Index.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               getCurrentSone(freenetRequest.toadletContext)!!.let { currentSone ->
+                       (currentSone.posts +
+                                       currentSone.friends
+                                                       .map { webInterface.core.getSone(it) }
+                                                       .filter { it.isPresent }
+                                                       .map { it.get() }
+                                                       .flatMap { it.posts } +
+                                       webInterface.core.getDirectedPosts(currentSone.id)
+                                       ).distinct()
+                                       .filter { postVisibilityFilter.isVisible(currentSone).apply(it) }
+                                       .sortedByDescending { it.time }
+                                       .let { posts ->
+                                               Pagination(posts, webInterface.core.preferences.postsPerPage).apply {
+                                                       page = freenetRequest.parameters["page"]?.toIntOrNull() ?: 0
+                                               }.let { pagination ->
+                                                       templateContext["pagination"] = pagination
+                                                       templateContext["posts"] = pagination.items
+                                               }
+                                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/KnownSonesPage.kt
new file mode 100644 (file)
index 0000000..9d6930d
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.parameters
+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 shows all known Sones.
+ */
+class KnownSonesPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("knownSones.html", template, "Page.KnownSones.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               getCurrentSone(freenetRequest.toadletContext).let { currentSone ->
+                       Pagination(webInterface.core.sones
+                                       .filterNot { freenetRequest.parameters["filter"] == "followed" && currentSone != null && !currentSone.hasFriend(it.id) }
+                                       .filterNot { freenetRequest.parameters["filter"] == "not-followed" && currentSone != null && currentSone.hasFriend(it.id) }
+                                       .filterNot { freenetRequest.parameters["filter"] == "new" && it.isKnown }
+                                       .filterNot { freenetRequest.parameters["filter"] == "not-new" && !it.isKnown }
+                                       .filterNot { freenetRequest.parameters["filter"] == "own" && !it.isLocal }
+                                       .filterNot { freenetRequest.parameters["filter"] == "not-own" && it.isLocal }
+                                       .sortedWith(
+                                                       when (freenetRequest.parameters["sort"]) {
+                                                               "images" -> Sone.IMAGE_COUNT_COMPARATOR
+                                                               "name" -> Sone.NICE_NAME_COMPARATOR.reversed()
+                                                               "posts" -> Sone.POST_COUNT_COMPARATOR
+                                                               else -> Sone.LAST_ACTIVITY_COMPARATOR
+                                                       }.let { comparator ->
+                                                               when (freenetRequest.parameters["order"]) {
+                                                                       "asc" -> comparator.reversed()
+                                                                       else -> comparator
+                                                               }
+                                                       }
+                                       ), 25).apply { page = freenetRequest.parameters["page"]?.toIntOrNull() ?: 0 }
+                                       .let { pagination ->
+                                               templateContext["pagination"] = pagination
+                                               templateContext["knownSones"] = pagination.items
+                                       }
+                       templateContext["sort"] = freenetRequest.parameters["sort"].let { sort -> if (sort in listOf("images", "name", "posts")) sort else "activity" }
+                       templateContext["order"] = freenetRequest.parameters["order"].let { order -> if (order == "asc") "asc" else "desc" }
+                       templateContext["filter"] = freenetRequest.parameters["filter"]
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LikePage.kt
new file mode 100644 (file)
index 0000000..43753a3
--- /dev/null
@@ -0,0 +1,30 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 like [net.pterodactylus.sone.data.Post]s and [net.pterodactylus.sone.data.Reply]s.
+ */
+class LikePage(template: Template, webInterface: WebInterface)
+       : SoneTemplatePage("like.html", template, "Page.Like.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       getCurrentSone(freenetRequest.toadletContext)!!.let { currentSone ->
+                               freenetRequest.parameters["type", 16]?.also { type ->
+                                       when(type) {
+                                               "post" -> currentSone.addLikedPostId(freenetRequest.parameters["post", 36]!!)
+                                               "reply" -> currentSone.addLikedReplyId(freenetRequest.parameters["reply", 36]!!)
+                                       }
+                               }
+                               throw RedirectException(freenetRequest.parameters["returnPage", 256]!!)
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LockSonePage.kt
new file mode 100644 (file)
index 0000000..835fe23
--- /dev/null
@@ -0,0 +1,24 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.parameters
+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 [net.pterodactylus.sone.data.Sone] to prevent it from being inserted.
+ */
+class LockSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("lockSone.html", template, "Page.LockSone.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               freenetRequest.parameters["returnPage", 256]!!.let { returnPage ->
+                       freenetRequest.parameters["sone", 44]!!
+                                       .let { webInterface.core.getLocalSone(it) }
+                                       ?.let { webInterface.core.lockSone(it) }
+                       throw RedirectException(returnPage)
+               }
+       }
+
+}
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..0e0fee5
--- /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.emptyToNull
+import net.pterodactylus.sone.utils.isPOST
+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(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val soneId = freenetRequest.httpRequest.getPartAsStringFailsafe("sone-id", 43)
+                       webInterface.core.getLocalSone(soneId)?.let { sone ->
+                               setCurrentSone(freenetRequest.toadletContext, sone)
+                               val target = freenetRequest.httpRequest.getParam("target").emptyToNull ?: "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(freenetRequest: FreenetRequest) =
+                       getCurrentSone(freenetRequest.toadletContext)?.let { "index.html" }
+
+       override fun isEnabled(toadletContext: ToadletContext) = when {
+               webInterface.core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess -> false
+               else -> getCurrentSone(toadletContext, false) == null
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/LogoutPage.kt
new file mode 100644 (file)
index 0000000..3a2a023
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import freenet.clients.http.ToadletContext
+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
+
+/**
+ * Logs a user out.
+ */
+class LogoutPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("logout.html", template, "Page.Logout.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               setCurrentSone(freenetRequest.toadletContext, null)
+               throw RedirectException("index.html")
+       }
+
+       override fun isEnabled(toadletContext: ToadletContext): Boolean =
+                       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/MarkAsKnownPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MarkAsKnownPage.kt
new file mode 100644 (file)
index 0000000..570e570
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.utils.mapPresent
+import net.pterodactylus.sone.utils.parameters
+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 mark a number of [net.pterodactylus.sone.data.Sone]s, [Post]s, or
+ * [Replie][net.pterodactylus.sone.data.Reply]s as known.
+ */
+class MarkAsKnownPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("markAsKnown.html", template, "Page.MarkAsKnown.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val ids = freenetRequest.parameters["id", 65536]!!.split(" ")
+               when (freenetRequest.parameters["type", 5]) {
+                       "sone" -> ids.mapPresent(webInterface.core::getSone).forEach(webInterface.core::markSoneKnown)
+                       "post" -> ids.mapPresent(webInterface.core::getPost).forEach(webInterface.core::markPostKnown)
+                       "reply" -> ids.mapPresent(webInterface.core::getPostReply).forEach(webInterface.core::markReplyKnown)
+                       else -> throw RedirectException("invalid.html")
+               }
+               throw RedirectException(freenetRequest.parameters["returnPage", 256]!!)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/NewPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/NewPage.kt
new file mode 100644 (file)
index 0000000..55df67a
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.mapPresent
+import net.pterodactylus.sone.utils.parameters
+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 displays all new posts and replies. The posts are filtered using
+ * [PostVisibilityFilter.isPostVisible(Sone, Post)] and sorted by time.
+ */
+class NewPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("new.html", template, "Page.New.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) =
+                       getCurrentSone(freenetRequest.toadletContext).let { currentSone ->
+                               (webInterface.getNewPosts(currentSone) + webInterface.getNewReplies(currentSone).mapPresent { it.post })
+                                               .distinct()
+                                               .sortedByDescending { it.time }
+                                               .let { posts ->
+                                                       Pagination(posts, webInterface.core.preferences.postsPerPage).apply {
+                                                               page = freenetRequest.parameters["page"]?.toIntOrNull() ?: 0
+                                                       }.let { pagination ->
+                                                               templateContext["pagination"] = pagination
+                                                               templateContext["posts"] = pagination.items
+                                                       }
+                                               }
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/OptionsPage.kt
new file mode 100644 (file)
index 0000000..7cf1bb3
--- /dev/null
@@ -0,0 +1,104 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.core.Preferences
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 edit the options of the Sone plugin.
+ */
+class OptionsPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("options.html", template, "Page.Options.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val fieldsWithErrors = mutableListOf<String>()
+                       getCurrentSone(freenetRequest.toadletContext)?.options?.let { options ->
+                               val autoFollow = "auto-follow" in freenetRequest.parameters
+                               val loadLinkedImages = freenetRequest.parameters["load-linked-images"].emptyToNull
+                               val showCustomAvatars = freenetRequest.parameters["show-custom-avatars"].emptyToNull
+                               val enableSoneInsertNotification = "enable-sone-insert-notifications" in freenetRequest.parameters
+                               val showNewSoneNotification = "show-notification-new-sones" in freenetRequest.parameters
+                               val showNewPostNotification = "show-notification-new-posts" in freenetRequest.parameters
+                               val showNewReplyNotification = "show-notification-new-replies" in freenetRequest.parameters
+
+                               options.isAutoFollow = autoFollow
+                               options.isSoneInsertNotificationEnabled = enableSoneInsertNotification
+                               options.isShowNewSoneNotifications = showNewSoneNotification
+                               options.isShowNewPostNotifications = showNewPostNotification
+                               options.isShowNewReplyNotifications = showNewReplyNotification
+                               loadLinkedImages?.also { if (cantSetOption { options.loadLinkedImages = LoadExternalContent.valueOf(loadLinkedImages) }) fieldsWithErrors += "load-linked-images" }
+                               showCustomAvatars?.also { if (cantSetOption { options.showCustomAvatars = LoadExternalContent.valueOf(showCustomAvatars) }) fieldsWithErrors += "show-custom-avatars" }
+                       }
+                       val fullAccessRequired = "require-full-access" in freenetRequest.parameters
+                       val fcpInterfaceActive = "fcp-interface-active" in freenetRequest.parameters
+
+                       webInterface.core.preferences.isRequireFullAccess = fullAccessRequired
+                       webInterface.core.preferences.isFcpInterfaceActive = fcpInterfaceActive
+
+                       val postsPerPage = freenetRequest.parameters["posts-per-page"]?.toIntOrNull()
+                       val charactersPerPost = freenetRequest.parameters["characters-per-post"]?.toIntOrNull()
+                       val postCutOffLength = freenetRequest.parameters["post-cut-off-length"]?.toIntOrNull()
+                       val imagesPerPage = freenetRequest.parameters["images-per-page"]?.toIntOrNull()
+                       val insertionDelay = freenetRequest.parameters["insertion-delay"]?.toIntOrNull()
+                       val fcpFullAccessRequired = freenetRequest.parameters["fcp-full-access-required"]?.toIntOrNull()
+                       val negativeTrust = freenetRequest.parameters["negative-trust"]?.toIntOrNull()
+                       val positiveTrust = freenetRequest.parameters["positive-trust"]?.toIntOrNull()
+                       val trustComment = freenetRequest.parameters["trust-comment"]?.emptyToNull
+
+                       if (cantSetOption { it.setPostsPerPage(postsPerPage) }) fieldsWithErrors += "posts-per-page"
+                       if (cantSetOption { it.setCharactersPerPost(charactersPerPost) }) fieldsWithErrors += "characters-per-post"
+                       if (cantSetOption { it.setPostCutOffLength(postCutOffLength) }) fieldsWithErrors += "post-cut-off-length"
+                       if (cantSetOption { it.setImagesPerPage(imagesPerPage) }) fieldsWithErrors += "images-per-page"
+                       if (cantSetOption { it.setInsertionDelay(insertionDelay) }) fieldsWithErrors += "insertion-delay"
+                       fcpFullAccessRequired?.also { if (cantSetOption { it.fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequired] }) fieldsWithErrors += "fcp-full-access-required" }
+                       if (cantSetOption { it.setNegativeTrust(negativeTrust) }) fieldsWithErrors += "negative-trust"
+                       if (cantSetOption { it.setPositiveTrust(positiveTrust) }) fieldsWithErrors += "positive-trust"
+                       if (cantSetOption { it.trustComment = trustComment }) fieldsWithErrors += "trust-comment"
+
+                       if (fieldsWithErrors.isEmpty()) {
+                               webInterface.core.touchConfiguration()
+                               throw RedirectException("options.html")
+                       }
+                       templateContext["fieldErrors"] = fieldsWithErrors
+               }
+               getCurrentSone(freenetRequest.toadletContext)?.options?.let { options ->
+                       templateContext["auto-follow"] = options.isAutoFollow
+                       templateContext["show-notification-new-sones"] = options.isShowNewSoneNotifications
+                       templateContext["show-notification-new-posts"] = options.isShowNewPostNotifications
+                       templateContext["show-notification-new-replies"] = options.isShowNewReplyNotifications
+                       templateContext["enable-sone-insert-notifications"] = options.isSoneInsertNotificationEnabled
+                       templateContext["load-linked-images"] = options.loadLinkedImages.toString()
+                       templateContext["show-custom-avatars"] = options.showCustomAvatars.toString()
+               }
+               webInterface.core.preferences.let { preferences ->
+                       templateContext["insertion-delay"] = preferences.insertionDelay
+                       templateContext["characters-per-post"] = preferences.charactersPerPost
+                       templateContext["fcp-full-access-required"] = preferences.fcpFullAccessRequired.ordinal
+                       templateContext["images-per-page"] = preferences.imagesPerPage
+                       templateContext["fcp-interface-active"] = preferences.isFcpInterfaceActive
+                       templateContext["require-full-access"] = preferences.isRequireFullAccess
+                       templateContext["negative-trust"] = preferences.negativeTrust
+                       templateContext["positive-trust"] = preferences.positiveTrust
+                       templateContext["post-cut-off-length"] = preferences.postCutOffLength
+                       templateContext["posts-per-page"] = preferences.postsPerPage
+                       templateContext["trust-comment"] = preferences.trustComment
+               }
+       }
+
+       private fun cantSetOption(setter: (Preferences) -> Unit) =
+                       try {
+                               setter(webInterface.core.preferences)
+                               false
+                       } catch (iae: IllegalArgumentException) {
+                               true
+                       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ReloadingPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ReloadingPage.kt
new file mode 100644 (file)
index 0000000..697d173
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.util.web.Page
+import net.pterodactylus.util.web.Request
+import net.pterodactylus.util.web.Response
+import java.io.File
+
+/**
+ * [Page] implementation that delivers static files from the filesystem.
+ */
+class ReloadingPage<R: Request>(private val prefix: String, private val path: String, private val mimeType: String): Page<R> {
+
+       override fun isPrefixPage() = true
+
+       override fun getPath() = prefix
+
+       override fun handleRequest(request: R, response: Response): Response {
+               val filename = request.uri.path.split("/").last()
+               File(path, filename).also { file ->
+                       if (file.exists()) {
+                               response.content.use { output ->
+                                       file.forEachBlock { buffer, bytesRead ->
+                                               output.write(buffer, 0, bytesRead)
+                                       }
+                               }
+                               response.statusCode = 200
+                               response.contentType = mimeType
+                       } else {
+                               response.statusCode = 404
+                               response.statusText = "Not found"
+                       }
+               }
+               return response
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/RescuePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/RescuePage.kt
new file mode 100644 (file)
index 0000000..dbcf59f
--- /dev/null
@@ -0,0 +1,32 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 control the rescue mode for a Sone.
+ */
+class RescuePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("rescue.html", template, "Page.Rescue.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val soneRescuer = webInterface.core.getSoneRescuer(getCurrentSone(freenetRequest.toadletContext)!!)
+               templateContext["soneRescuer"] = soneRescuer
+               if (freenetRequest.isPOST) {
+                       freenetRequest.parameters["edition", 9]?.toIntOrNull()?.also {
+                               if (it > -1) {
+                                       soneRescuer.setEdition(it.toLong())
+                               }
+                       }
+                       if (freenetRequest.parameters["fetch", 8] == "true") {
+                               soneRescuer.startNextFetch()
+                       }
+                       throw RedirectException("rescue.html")
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt
new file mode 100644 (file)
index 0000000..aff6aaf
--- /dev/null
@@ -0,0 +1,145 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Ticker
+import com.google.common.cache.Cache
+import com.google.common.cache.CacheBuilder
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.paginate
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.sone.web.pages.SearchPage.Optionality.FORBIDDEN
+import net.pterodactylus.sone.web.pages.SearchPage.Optionality.OPTIONAL
+import net.pterodactylus.sone.web.pages.SearchPage.Optionality.REQUIRED
+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 java.util.concurrent.TimeUnit.MINUTES
+
+/**
+ * This page lets the user search for posts and replies that contain certain
+ * words.
+ */
+class SearchPage @JvmOverloads constructor(template: Template, webInterface: WebInterface, ticker: Ticker = Ticker.systemTicker()):
+               SoneTemplatePage("search.html", template, "Page.Search.Title", webInterface, false) {
+
+       private val cache: Cache<Iterable<Phrase>, Pagination<Post>> = CacheBuilder.newBuilder().ticker(ticker).expireAfterAccess(5, MINUTES).build()
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               val phrases = try {
+                       freenetRequest.parameters["query"].emptyToNull?.parse()
+               } catch (te: TextException) {
+                       redirect("index.html")
+               }
+                               ?: redirect("index.html")
+
+               when (phrases.size) {
+                       0 -> redirect("index.html")
+                       1 -> phrases.first().phrase.also { word ->
+                               when {
+                                       word.removePrefix("sone://").let(webInterface.core::getSone).isPresent -> redirect("viewSone.html?sone=${word.removePrefix("sone://")}")
+                                       word.removePrefix("post://").let(webInterface.core::getPost).isPresent -> redirect("viewPost.html?post=${word.removePrefix("post://")}")
+                                       word.removePrefix("reply://").let(webInterface.core::getPostReply).isPresent -> redirect("viewPost.html?post=${word.removePrefix("reply://").let(webInterface.core::getPostReply).get().postId}")
+                                       word.removePrefix("album://").let(webInterface.core::getAlbum) != null -> redirect("imageBrowser.html?album=${word.removePrefix("album://")}")
+                                       word.removePrefix("image://").let { webInterface.core.getImage(it, false) } != null -> redirect("imageBrowser.html?image=${word.removePrefix("image://")}")
+                               }
+                       }
+               }
+
+               val sonePagination = webInterface.core.sones
+                               .scoreAndPaginate(phrases) { it.allText() }
+                               .apply { page = freenetRequest.parameters["sonePage"].emptyToNull?.toIntOrNull() ?: 0 }
+               val postPagination = cache.get(phrases) {
+                       webInterface.core.sones
+                                       .flatMap(Sone::getPosts)
+                                       .filter { Post.FUTURE_POSTS_FILTER.apply(it) }
+                                       .scoreAndPaginate(phrases) { it.allText() }
+               }.apply { page = freenetRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 }
+
+               templateContext["sonePagination"] = sonePagination
+               templateContext["soneHits"] = sonePagination.items
+               templateContext["postPagination"] = postPagination
+               templateContext["postHits"] = postPagination.items
+       }
+
+       private fun <T> Iterable<T>.scoreAndPaginate(phrases: Iterable<Phrase>, texter: (T) -> String) =
+                       map { it to score(texter(it), phrases) }
+                                       .filter { it.second > 0 }
+                                       .sortedByDescending { it.second }
+                                       .map { it.first }
+                                       .paginate(webInterface.core.preferences.postsPerPage)
+
+       private fun Sone.names() =
+                       listOf(name, profile.firstName, profile.middleName, profile.lastName)
+                                       .filterNotNull()
+                                       .joinToString("")
+
+       private fun Sone.allText() =
+                       (names() + profile.fields.map { "${it.name} ${it.value}" }.joinToString(" ", " ")).toLowerCase()
+
+       private fun Post.allText() =
+                       (text + recipient.orNull()?.let { " ${it.names()}" } + webInterface.core.getReplies(id)
+                                       .filter { PostReply.FUTURE_REPLY_FILTER.apply(it) }
+                                       .map { "${it.sone.names()} ${it.text}" }.joinToString(" ", " ")).toLowerCase()
+
+       private fun score(text: String, phrases: Iterable<Phrase>): Double {
+               val requiredPhrases = phrases.count { it.required }
+               val requiredHits = phrases.filter(Phrase::required)
+                               .map(Phrase::phrase)
+                               .flatMap { text.findAll(it) }
+                               .map { Math.pow(1 - it / text.length.toDouble(), 2.0) }
+                               .sum()
+               val optionalHits = phrases.filter(Phrase::optional)
+                               .map(Phrase::phrase)
+                               .flatMap { text.findAll(it) }
+                               .map { Math.pow(1 - it / text.length.toDouble(), 2.0) }
+                               .sum()
+               val forbiddenHits = phrases.filter(Phrase::forbidden)
+                               .map(Phrase::phrase)
+                               .map { text.findAll(it).size }
+                               .sum()
+               return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2)
+       }
+
+       private fun String.findAll(needle: String): List<Int> {
+               var nextIndex = indexOf(needle)
+               val positions = mutableListOf<Int>()
+               while (nextIndex != -1) {
+                       positions += nextIndex
+                       nextIndex = indexOf(needle, nextIndex + 1)
+               }
+               return positions
+       }
+
+       private fun String.parse() =
+                       StringEscaper.parseLine(this)
+                                       .map(String::toLowerCase)
+                                       .map {
+                                               when {
+                                                       it == "+" || it == "-" -> Phrase(it, OPTIONAL)
+                                                       it.startsWith("+") -> Phrase(it.drop(1), REQUIRED)
+                                                       it.startsWith("-") -> Phrase(it.drop(1), FORBIDDEN)
+                                                       else -> Phrase(it, OPTIONAL)
+                                               }
+                                       }
+
+       private fun redirect(target: String): Nothing = throw RedirectException(target)
+
+       enum class Optionality {
+               OPTIONAL,
+               REQUIRED,
+               FORBIDDEN
+       }
+
+       private data class Phrase(val phrase: String, val optionality: Optionality) {
+               val required = optionality == REQUIRED
+               val forbidden = optionality == FORBIDDEN
+               val optional = optionality == OPTIONAL
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SoneTemplatePage.kt
new file mode 100644 (file)
index 0000000..3f04236
--- /dev/null
@@ -0,0 +1,101 @@
+package net.pterodactylus.sone.web.pages
+
+import freenet.clients.http.ToadletContext
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.main.SonePlugin
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.web.SessionProvider
+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 java.net.URLEncoder
+
+/**
+ * Base page for the Sone web interface.
+ */
+open class SoneTemplatePage(
+               path: String,
+               protected val webInterface: WebInterface,
+               template: Template,
+               private val pageTitleKey: String? = null,
+               private val requiresLogin: Boolean = true
+) : FreenetTemplatePage(path, webInterface.templateContextFactory, template, "noPermission.html") {
+
+       @JvmOverloads
+       constructor(path: String, template: Template, pageTitleKey: String?, webInterface: WebInterface, requireLogin: Boolean = false) :
+                       this(path, webInterface, template, pageTitleKey, requireLogin)
+
+       constructor(path: String, template: Template, webInterface: WebInterface, requireLogin: Boolean = true) :
+                       this(path, webInterface, template, null, requireLogin)
+
+       private val core = webInterface.core
+       protected val sessionProvider: SessionProvider = webInterface
+
+       protected fun getCurrentSone(toadletContext: ToadletContext, createSession: Boolean = true) =
+                       sessionProvider.getCurrentSone(toadletContext, createSession)
+
+       protected fun setCurrentSone(toadletContext: ToadletContext, sone: Sone?) =
+                       sessionProvider.setCurrentSone(toadletContext, sone)
+
+       fun requiresLogin() = requiresLogin
+
+       override public fun getPageTitle(freenetRequest: FreenetRequest) =
+                       pageTitleKey?.let(webInterface.l10n::getString) ?: ""
+
+       override public fun getStyleSheets() =
+                       listOf("css/sone.css")
+
+       override public fun getShortcutIcon() = "images/icon.png"
+
+       override public fun getAdditionalLinkNodes(request: FreenetRequest) =
+                       listOf(mapOf(
+                                       "rel" to "search",
+                                       "type" to "application/opensearchdescription+xml",
+                                       "title" to "Sone",
+                                       "href" to "http://${request.httpRequest.getHeader("host")}/Sone/OpenSearch.xml"
+                       ))
+
+       final override public fun processTemplate(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               super.processTemplate(freenetRequest, templateContext)
+               templateContext["preferences"] = core.preferences
+               templateContext["currentSone"] = getCurrentSone(freenetRequest.toadletContext)
+               templateContext["localSones"] = core.localSones
+               templateContext["request"] = freenetRequest
+               templateContext["currentVersion"] = SonePlugin.getPluginVersion()
+               templateContext["hasLatestVersion"] = core.updateChecker.hasLatestVersion()
+               templateContext["latestEdition"] = core.updateChecker.latestEdition
+               templateContext["latestVersion"] = core.updateChecker.latestVersion
+               templateContext["latestVersionTime"] = core.updateChecker.latestVersionDate
+               webInterface.getNotifications(getCurrentSone(freenetRequest.toadletContext)).sortedBy(Notification::getCreatedTime).run {
+                       templateContext["notifications"] = this
+                       templateContext["notificationHash"] = this.hashCode()
+               }
+               handleRequest(freenetRequest, templateContext)
+       }
+
+       internal open fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+       }
+
+       override public fun getRedirectTarget(freenetRequest: FreenetRequest): String? {
+               if (requiresLogin && getCurrentSone(freenetRequest.toadletContext) == null) {
+                       val parameters = freenetRequest.httpRequest.parameterNames
+                                       .flatMap { name -> freenetRequest.httpRequest.getMultipleParam(name).map { name to it } }
+                                       .joinToString("&") { "${it.first.urlEncode}=${it.second.urlEncode}" }
+                                       .emptyToNull
+                       return "login.html?target=${freenetRequest.httpRequest.path}${parameters?.let { ("?" + it).urlEncode } ?: ""}"
+               }
+               return null
+       }
+
+       private val String.urlEncode: String get() = URLEncoder.encode(this, "UTF-8")
+
+       override fun isEnabled(toadletContext: ToadletContext) = when {
+               requiresLogin && getCurrentSone(toadletContext) == null -> false
+               core.preferences.isRequireFullAccess && !toadletContext.isAllowedFullAccess -> false
+               else -> true
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/TrustPage.kt
new file mode 100644 (file)
index 0000000..bdc8952
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+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 trust another Sone. This will assign a configurable
+ * amount of trust to an identity.
+ */
+class TrustPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("trust.html", template, "Page.Trust.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       getCurrentSone(freenetRequest.toadletContext)?.also { currentSone ->
+                               webInterface.core.getSone(freenetRequest.parameters["sone"]).let { sone ->
+                                       webInterface.core.trustSone(currentSone, sone)
+                               }
+                       }
+                       throw RedirectException(freenetRequest.parameters["returnPage", 256])
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnbookmarkPage.kt
new file mode 100644 (file)
index 0000000..01a0fde
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.isGET
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 unbookmark a post.
+ */
+class UnbookmarkPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("unbookmark.html", template, "Page.Unbookmark.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               when {
+                       freenetRequest.isGET && (freenetRequest.parameters["post"] == "allNotLoaded") -> {
+                               webInterface.core.bookmarkedPosts
+                                               .filterNot(Post::isLoaded)
+                                               .forEach(webInterface.core::unbookmarkPost)
+                               throw RedirectException("bookmarks.html")
+                       }
+                       freenetRequest.isPOST -> {
+                               freenetRequest.parameters["post", 36]
+                                               .let(webInterface.core::getPost)
+                                               .also(webInterface.core::unbookmarkPost)
+                               throw RedirectException(freenetRequest.parameters["returnPage", 256])
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnfollowSonePage.kt
new file mode 100644 (file)
index 0000000..cc411e2
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 unfollow another Sone.
+ */
+class UnfollowSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("unfollowSone.html", template, "Page.UnfollowSone.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       getCurrentSone(freenetRequest.toadletContext)!!.also { currentSone ->
+                               freenetRequest.parameters["sone"]!!.split(Regex("[ ,]+"))
+                                               .forEach { webInterface.core.unfollowSone(currentSone, it) }
+                       }
+                       throw RedirectException(freenetRequest.parameters["returnPage", 256])
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlikePage.kt
new file mode 100644 (file)
index 0000000..42e552e
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 unlike a [net.pterodactylus.sone.data.Post] or [net.pterodactylus.sone.data.Reply].
+ */
+class UnlikePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("unlike.html", template, "Page.Unlike.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       when (freenetRequest.parameters["type"]) {
+                               "post" -> getCurrentSone(freenetRequest.toadletContext)!!.removeLikedPostId(freenetRequest.parameters["post"]!!)
+                               "reply" -> getCurrentSone(freenetRequest.toadletContext)!!.removeLikedReplyId(freenetRequest.parameters["reply"]!!)
+                       }
+                       throw RedirectException(freenetRequest.parameters["returnPage", 256])
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UnlockSonePage.kt
new file mode 100644 (file)
index 0000000..e792318
--- /dev/null
@@ -0,0 +1,25 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 [net.pterodactylus.sone.data.Sone] to allow its insertion.
+ */
+class UnlockSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("unlockSone.html", template, "Page.UnlockSone.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       freenetRequest.parameters["sone", 44]
+                                       .let(webInterface.core::getLocalSone)
+                                       ?.also(webInterface.core::unlockSone)
+                       throw RedirectException(freenetRequest.parameters["returnPage", 256])
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UntrustPage.kt
new file mode 100644 (file)
index 0000000..a46b272
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.utils.also
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+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 untrust another Sone. This will remove all trust
+ * assignments for an identity.
+ */
+class UntrustPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("untrust.html", template, "Page.Untrust.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       getCurrentSone(freenetRequest.toadletContext)!!.also { currentSone ->
+                               freenetRequest.parameters["sone", 44]
+                                               .let(webInterface.core::getSone)
+                                               .also { webInterface.core.untrustSone(currentSone, it) }
+                       }
+                       throw RedirectException(freenetRequest.parameters["returnPage", 256])
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/UploadImagePage.kt
new file mode 100644 (file)
index 0000000..782c90d
--- /dev/null
@@ -0,0 +1,71 @@
+package net.pterodactylus.sone.web.pages
+
+import freenet.support.api.Bucket
+import net.pterodactylus.sone.text.TextFilter
+import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.headers
+import net.pterodactylus.sone.utils.isPOST
+import net.pterodactylus.sone.utils.parameters
+import net.pterodactylus.sone.utils.use
+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.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import javax.imageio.ImageIO
+
+/**
+ * Page implementation that lets the user upload an image.
+ */
+class UploadImagePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("uploadImage.html", template, "Page.UploadImage.Title", webInterface, true) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               if (freenetRequest.isPOST) {
+                       val parentAlbum = freenetRequest.parameters["parent"]!!.let(webInterface.core::getAlbum) ?: throw RedirectException("noPermission.html")
+                       if (parentAlbum.sone != getCurrentSone(freenetRequest.toadletContext)) {
+                               throw RedirectException("noPermission.html")
+                       }
+                       val title = freenetRequest.parameters["title", 200].emptyToNull ?: throw RedirectException("emptyImageTitle.html")
+
+                       val uploadedFile = freenetRequest.httpRequest.getUploadedFile("image")
+                       val bytes = uploadedFile.data.use { it.toByteArray() }
+                       val bufferedImage = bytes.toImage()
+                       if (bufferedImage == null) {
+                               templateContext["messages"] = webInterface.l10n.getString("Page.UploadImage.Error.InvalidImage")
+                               return
+                       }
+
+                       val temporaryImage = webInterface.core.createTemporaryImage(bytes.mimeType, bytes)
+                       webInterface.core.createImage(getCurrentSone(freenetRequest.toadletContext), parentAlbum, temporaryImage).modify().apply {
+                               setWidth(bufferedImage.width)
+                               setHeight(bufferedImage.height)
+                               setTitle(title)
+                               setDescription(TextFilter.filter(freenetRequest.headers["Host"], freenetRequest.parameters["description", 4000]))
+                       }.update()
+                       throw RedirectException("imageBrowser.html?album=${parentAlbum.id}")
+               }
+       }
+
+       private fun Bucket.toByteArray(): ByteArray = ByteArrayOutputStream(size().toInt()).use { outputStream ->
+               inputStream.copyTo(outputStream)
+               outputStream.toByteArray()
+       }
+
+       private fun ByteArray.toImage(): BufferedImage? = ByteArrayInputStream(this).use {
+               ImageIO.read(it)
+       }
+
+       private val ByteArray.mimeType get() = ByteArrayInputStream(this).use {
+               ImageIO.createImageInputStream(it).use {
+                       ImageIO.getImageReaders(it).asSequence()
+                                       .firstOrNull()?.originatingProvider?.mimeTypes?.firstOrNull()
+                                       ?: UNKNOWN_MIME_TYPE
+               }
+       }
+
+}
+
+private const val UNKNOWN_MIME_TYPE = "application/octet-stream"
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewPostPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewPostPage.kt
new file mode 100644 (file)
index 0000000..457e13f
--- /dev/null
@@ -0,0 +1,34 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.template.SoneAccessor
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.parameters
+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.net.URI
+
+/**
+ * This page lets the user view a post and all its replies.
+ */
+class ViewPostPage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("viewPost.html", template, "Page.ViewPost.Title", webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               templateContext["post"] = freenetRequest.parameters["post"].let(webInterface.core::getPost).orNull()
+               templateContext["raw"] = freenetRequest.parameters["raw"] == "true"
+       }
+
+       override fun isLinkExcepted(link: URI?) = true
+
+       public override fun getPageTitle(freenetRequest: FreenetRequest) =
+                       (freenetRequest.parameters["post"].let(webInterface.core::getPost).let {
+                               if (it.text.length > 20) {
+                                       it.text.substring(0..19) + "…"
+                               } else {
+                                       it.text
+                               } + " - ${SoneAccessor.getNiceName(it.sone)} - "
+                       } ?: "") + super.getPageTitle(freenetRequest)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/ViewSonePage.kt
new file mode 100644 (file)
index 0000000..90bc147
--- /dev/null
@@ -0,0 +1,58 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.template.SoneAccessor
+import net.pterodactylus.sone.utils.let
+import net.pterodactylus.sone.utils.mapPresent
+import net.pterodactylus.sone.utils.paginate
+import net.pterodactylus.sone.utils.parameters
+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.net.URI
+
+/**
+ * Lets the user browser another Sone.
+ */
+class ViewSonePage(template: Template, webInterface: WebInterface):
+               SoneTemplatePage("viewSone.html", template, webInterface, false) {
+
+       override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+               templateContext["soneId"] = freenetRequest.parameters["sone"]
+               freenetRequest.parameters["sone"].let(webInterface.core::getSone).let { sone ->
+                       templateContext["sone"] = sone
+                       val sonePosts = sone.posts
+                       val directedPosts = webInterface.core.getDirectedPosts(sone.id)
+                       (sonePosts + directedPosts)
+                                       .sortedByDescending(Post::getTime)
+                                       .paginate(webInterface.core.preferences.postsPerPage)
+                                       .apply { page = freenetRequest.parameters["postPage"]?.toIntOrNull() ?: 0 }
+                                       .also {
+                                               templateContext["postPagination"] = it
+                                               templateContext["posts"] = it.items
+                                       }
+                       sone.replies
+                                       .mapPresent(PostReply::getPost)
+                                       .distinct()
+                                       .minus(sonePosts)
+                                       .minus(directedPosts)
+                                       .sortedByDescending { webInterface.core.getReplies(it.id).first().time }
+                                       .paginate(webInterface.core.preferences.postsPerPage)
+                                       .apply { page = freenetRequest.parameters["repliedPostPage"]?.toIntOrNull() ?: 0 }
+                                       .also {
+                                               templateContext["repliedPostPagination"] = it
+                                               templateContext["repliedPosts"] = it.items
+                                       }
+               }
+       }
+
+       override fun isLinkExcepted(link: URI?) = true
+
+       public override fun getPageTitle(freenetRequest: FreenetRequest): String =
+                       freenetRequest.parameters["sone"].let(webInterface.core::getSone).let { sone ->
+                               "${SoneAccessor.getNiceName(sone)} - ${webInterface.l10n.getString("Page.ViewSone.Title")}"
+                       } ?: webInterface.l10n.getString("Page.ViewSone.Page.TitleWithoutSone")
+
+}
index 62e9807..e655691 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Nur benutzerdefinierte Avat
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Nur benutzerdefinierte Avatare von Sones, denen Sie manuell einen Vertrauenswert von mehr als 0 zugewiesen haben, anzeigen.
 Page.Options.Option.ShowAvatars.Trusted.Description=Nur benutzerdefinierte Avatare von Sones, die einen berechneten Vertrauenswert von mehr als 0 haben, anzeigen.
 Page.Options.Option.ShowAvatars.Always.Description=Immer benutzerdefinierte Avatare anzeigen. Warnung: Benutzerdefinierte Avatare können beliebiges Bildmaterial enthalten!
+Page.Options.Section.LoadLinkedImagesOptions.Title=Verlinkte Bilder laden
+Page.Options.Option.LoadLinkedImages.Description=Sone kann automatisch in Nachrichten und Antworten verlinkte Bilder laden. Diese Bilder werden nur aus Freenet geladen, niemals aus dem Internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Niemals verlinkte Bilder laden.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Nur Bilder aus Nachrichten von Sones, denen Sie folgen, laden.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Nur Bilder aus Nachrichten von Sones, denen Sie manuell einen Vertrauenswert von mehr als 0 zugewiesen haben, laden.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Nur Bilder aus Nachrichten von Sones, die einen berechneten Vertrauenswert von mehr als 0 haben, laden.
+Page.Options.Option.LoadLinkedImages.Always.Description=Immer verlinkte Bilder laden. Warnung: Verlinkte Bilder können beliebiges Bildmaterial enthalten!
 Page.Options.Section.RuntimeOptions.Title=Laufzeitverhalten
 Page.Options.Option.InsertionDelay.Description=Anzahl der Sekunden, die vor dem Hochladen einer Sone nach einer Änderung gewartet wird.
 Page.Options.Option.PostsPerPage.Description=Anzahl der Nachrichten pro Seite.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=in der Zukunft
 View.Time.AFewSecondsAgo=vor ein paar Sekunden
 View.Time.HalfAMinuteAgo=vor einer halben Minute
 View.Time.AMinuteAgo=vor ungefähr einer Minute
-View.Time.XMinutesAgo=vor ${min} Minuten
+View.Time.XMinutesAgo=vor {0} Minuten
 View.Time.HalfAnHourAgo=vor einer halben Stunde
 View.Time.AnHourAgo=vor ungefähr einer Stunde
-View.Time.XHoursAgo=vor ${hour} Stunden
+View.Time.XHoursAgo=vor {0} Stunden
 View.Time.ADayAgo=vor ungefähr einem Tag
-View.Time.XDaysAgo=vor ${day} Tagen
+View.Time.XDaysAgo=vor {0} Tagen
 View.Time.AWeekAgo=vor ungefähr einer Woche
-View.Time.XWeeksAgo=vor ${week} Wochen
+View.Time.XWeeksAgo=vor {0} Wochen
 View.Time.AMonthAgo=vor ungefähr einem Monat
-View.Time.XMonthsAgo=vor ${month} Monaten
+View.Time.XMonthsAgo=vor {0} Monaten
 View.Time.AYearAgo=vor ungefähr einem Jahr
-View.Time.XYearsAgo=vor ${year} Jahren
+View.Time.XYearsAgo=vor {0} Jahren
 
 WebInterface.DefaultText.StatusUpdate=Was beschäftigt Sie gerade?
 WebInterface.DefaultText.Message=Schreiben Sie eine Nachricht…
index ae57376..7b62cf1 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Only show avatars for Sones
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Only show avatars for Sones that you have manually assigned a trust value larger than 0 to.
 Page.Options.Option.ShowAvatars.Trusted.Description=Only show avatars for Sones that have a trust value larger than 0.
 Page.Options.Option.ShowAvatars.Always.Description=Always show custom avatars. Be warned: some avatars might contain disturbing or offensive imagery.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
 Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
 Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=in the future
 View.Time.AFewSecondsAgo=a few seconds ago
 View.Time.HalfAMinuteAgo=about half a minute ago
 View.Time.AMinuteAgo=about a minute ago
-View.Time.XMinutesAgo=${min} minutes ago
+View.Time.XMinutesAgo={0} minutes ago
 View.Time.HalfAnHourAgo=half an hour ago
 View.Time.AnHourAgo=about an hour ago
-View.Time.XHoursAgo=${hour} hours ago
+View.Time.XHoursAgo={0} hours ago
 View.Time.ADayAgo=about a day ago
-View.Time.XDaysAgo=${day} days ago
+View.Time.XDaysAgo={0} days ago
 View.Time.AWeekAgo=about a week ago
-View.Time.XWeeksAgo=${week} weeks ago
+View.Time.XWeeksAgo={0} weeks ago
 View.Time.AMonthAgo=about a month ago
-View.Time.XMonthsAgo=${month} months ago
+View.Time.XMonthsAgo={0} months ago
 View.Time.AYearAgo=about a year ago
-View.Time.XYearsAgo=${year} years ago
+View.Time.XYearsAgo={0} years ago
 
 WebInterface.DefaultText.StatusUpdate=What’s on your mind?
 WebInterface.DefaultText.Message=Write a Message…
index ab992e4..f5072c8 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Solo mostrar avatares de lo
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Solo mostrar avatares de Sones a los que has asignado manualmente un valor de veracidad mayor que 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Solo mostrar avatares para Sones que tienen un valor de veracidad mayor que 0.
 Page.Options.Option.ShowAvatars.Always.Description=Mostrar siempre avatares customizados. Advertencia, algunos avatares pueden contener imágenes perturbadoras o ofensivas.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Comportamiento de la ejecución.
 Page.Options.Option.InsertionDelay.Description=Número de segundos que se esperarán para insertar el Sone tras una modificación.
 Page.Options.Option.PostsPerPage.Description=Número de publicaciones que se mostrarán en una página antes de que se muestren los controles de navegación.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=en el futuro
 View.Time.AFewSecondsAgo=hace unos segundos
 View.Time.HalfAMinuteAgo=hace medio minuto
 View.Time.AMinuteAgo=hace un minuto
-View.Time.XMinutesAgo=hace ${min} minutos
+View.Time.XMinutesAgo=hace {0} minutos
 View.Time.HalfAnHourAgo=hace media hora
 View.Time.AnHourAgo=hace una hora
-View.Time.XHoursAgo=hace ${hour} horas
+View.Time.XHoursAgo=hace {0} horas
 View.Time.ADayAgo=hace un día
-View.Time.XDaysAgo=hace ${day} días
+View.Time.XDaysAgo=hace {0} días
 View.Time.AWeekAgo=hace una semana
-View.Time.XWeeksAgo=hace ${week} semanas
+View.Time.XWeeksAgo=hace {0} semanas
 View.Time.AMonthAgo=hace un mes
-View.Time.XMonthsAgo=hace ${month} meses
+View.Time.XMonthsAgo=hace {0} meses
 View.Time.AYearAgo=hace un año
-View.Time.XYearsAgo=hace ${year} años
+View.Time.XYearsAgo=hace {0} años
 
 WebInterface.DefaultText.StatusUpdate=Qué estás pensando?
 WebInterface.DefaultText.Message=Escribir un mensaje…
@@ -464,3 +471,4 @@ Notification.Mention.Text=Has sido mencionado en las siguientes publicaciones:
 Notification.SoneIsInserting.Text=Tu Sone sone://{0} está siendo insertado.
 Notification.SoneIsInserted.Text=Tu Sone sone://{0} ha sido insertado en {1,number} {1,choice,0#segundos|1#segundo|1<segundos}.
 Notification.SoneInsertAborted.Text=Tu Sone sone://{0} no pudo ser insertado.
+# 55-61
index e068e49..86015d0 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Ne montrer que les avatars
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Ne montrer que les avatars des Sones auxquelles vous êtes assignés manuellement une confiance superieure à 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Ne montrer que les avatars des Sones qui ont une confiance superieure à 0.
 Page.Options.Option.ShowAvatars.Always.Description=Montre tout le temps les avatars personnalisés. Attention: certains avatars peuvent être offensants !
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Comportement runtime
 Page.Options.Option.InsertionDelay.Description=Le délai d'insertion d'un Sone.
 Page.Options.Option.PostsPerPage.Description=Le nombre de message à afficher par page avant que les boutons de pagination soit affichés.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=dans le futur
 View.Time.AFewSecondsAgo=au cours des dernières secondes passées
 View.Time.HalfAMinuteAgo=au cours des 30 dernières secondes
 View.Time.AMinuteAgo=au cours de la dernière minute
-View.Time.XMinutesAgo=il y a environs ${min} minutes
+View.Time.XMinutesAgo=il y a environs {0} minutes
 View.Time.HalfAnHourAgo=au cours de la dernière demi heure
 View.Time.AnHourAgo=il y a environ une heure
-View.Time.XHoursAgo=Il y a environ ${hour} heures
+View.Time.XHoursAgo=Il y a environ {0} heures
 View.Time.ADayAgo=il y a environ un jour
-View.Time.XDaysAgo=il y a plus ou moins ${day} jours
+View.Time.XDaysAgo=il y a plus ou moins {0} jours
 View.Time.AWeekAgo=il y a environ une semaine
-View.Time.XWeeksAgo=au cours des dernières ${week}semaines
+View.Time.XWeeksAgo=au cours des dernières {0} semaines
 View.Time.AMonthAgo=au cours du dernier mois
-View.Time.XMonthsAgo=au cours des derniers ${month} mois
+View.Time.XMonthsAgo=au cours des derniers {0} mois
 View.Time.AYearAgo=au cours de la dernière année
-View.Time.XYearsAgo=au cours des dernières ${year} années
+View.Time.XYearsAgo=au cours des dernières {0} années
 
 WebInterface.DefaultText.StatusUpdate=Exprimez-vous
 WebInterface.DefaultText.Message=Écrire un message...
@@ -464,3 +471,4 @@ Notification.Mention.Text=Vous avez été mentionné dans les messages suivants:
 Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être inséré.
 Notification.SoneIsInserted.Text=votre Sone sone://{0} a été inséré dans {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Votre Sone sone://{0} ne peut pas être inséré.
+# 55-61
index d193e6f..bf81762 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=フォローしているSon
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=信用値を手動で0以上に設定しているSoneのみ、カスタムアイコンを表示する。
 Page.Options.Option.ShowAvatars.Trusted.Description=信用値が0以上のSoneのみ、カスタムアイコンを表示する。
 Page.Options.Option.ShowAvatars.Always.Description=常にカスタムアイコンを表示する。注意:不快な画像が表示される可能性があります。
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=実行の挙動
 Page.Options.Option.InsertionDelay.Description=Soneを変更した後にインサートが開始されるまでの遅延時間(秒)。
 Page.Options.Option.PostsPerPage.Description=ページ送りのボタンが表示されるまでに表示する投稿の数。
@@ -387,18 +394,18 @@ View.Time.InTheFuture=今より未来
 View.Time.AFewSecondsAgo=およそ数秒前
 View.Time.HalfAMinuteAgo=およそ30秒前
 View.Time.AMinuteAgo=およそ1分前
-View.Time.XMinutesAgo=${min}分前
+View.Time.XMinutesAgo={0}分前
 View.Time.HalfAnHourAgo=30分前
 View.Time.AnHourAgo=およそ1時間前
-View.Time.XHoursAgo=${hour}時間前
+View.Time.XHoursAgo={0}時間前
 View.Time.ADayAgo=およそ1日前
-View.Time.XDaysAgo=${day}日前
+View.Time.XDaysAgo={0}日前
 View.Time.AWeekAgo=およそ1週前
-View.Time.XWeeksAgo=${week}週前
+View.Time.XWeeksAgo={0}週前
 View.Time.AMonthAgo=およそ1月前
-View.Time.XMonthsAgo=${month}月前
+View.Time.XMonthsAgo={0}月前
 View.Time.AYearAgo=およそ1年前
-View.Time.XYearsAgo=${year}年前
+View.Time.XYearsAgo={0}年前
 
 WebInterface.DefaultText.StatusUpdate=何か投稿しますか?
 WebInterface.DefaultText.Message=メッセージを書いてください…
@@ -464,4 +471,4 @@ Notification.Mention.Text=次の投稿でメンションされています:
 Notification.SoneIsInserting.Text=あなたのSone sone://{0}は現在インサート中です。
 Notification.SoneIsInserted.Text=あなたのSone sone://{0}は{1,number}{1,choice,0#秒|1#秒|1<秒}でインサートされました。
 Notification.SoneInsertAborted.Text=あなたのSone sone://{0}のインサートに失敗しました。
-# 60, 100, 458
+# 55-51, 67, 107, 465
index 99a950f..5a13696 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Bare vis avatar for Soner d
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Bare vis avatar for Soner du manuelt har gitt en tillit større en 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Bare vis avatar for Soner som har en tillit større enn 0.
 Page.Options.Option.ShowAvatars.Always.Description=Alltid vis egendefinerte avatarer. Advarsel: Noen avatarer kan inneholde forstyrrende eller provoserende bilder.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Oppførsel ved kjøretid
 Page.Options.Option.InsertionDelay.Description=Antall sekunder Sone-innsetteren skal vente etter en endring av en Sone før den blir innsatt.
 Page.Options.Option.PostsPerPage.Description=Antallet innlegg å vise pr side før side-kontroller blir vist.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=i framtiden
 View.Time.AFewSecondsAgo=noen få sekunder siden
 View.Time.HalfAMinuteAgo=omtrent et halvt minutt siden
 View.Time.AMinuteAgo=omtrent et minutt siden
-View.Time.XMinutesAgo=${min} minutter siden
+View.Time.XMinutesAgo={0} minutter siden
 View.Time.HalfAnHourAgo=en halvtime siden
 View.Time.AnHourAgo=omtrent en time siden
-View.Time.XHoursAgo=${hour} timer siden
+View.Time.XHoursAgo={0} timer siden
 View.Time.ADayAgo=omtrent en dag siden
-View.Time.XDaysAgo=${day} dager siden
+View.Time.XDaysAgo={0} dager siden
 View.Time.AWeekAgo=omtrent en uke siden
-View.Time.XWeeksAgo=${week} uker siden
+View.Time.XWeeksAgo={0} uker siden
 View.Time.AMonthAgo=omtrent en måned siden
-View.Time.XMonthsAgo=${month} måneder siden
+View.Time.XMonthsAgo={0} måneder siden
 View.Time.AYearAgo=omtrent et år siden
-View.Time.XYearsAgo=${year} år siden
+View.Time.XYearsAgo={0} år siden
 
 WebInterface.DefaultText.StatusUpdate=Hva tenker du på?
 WebInterface.DefaultText.Message=Skriv et innlegg…
@@ -464,4 +471,4 @@ Notification.Mention.Text=Du har blitt nevnt i følgende innlegg:
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-# 60, 100, 120-121, 308-310, 312-314, 458, 463-465
+# 55-61, 67, 107, 127-128, 315-317, 319-321, 465, 471-473
index 82db1b5..e9e460b 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Pokazuj avatary tylko śled
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Pokazuj avatary tylko tych użytkowników Sone, którym ręcznie przyznałeś ilość punktów zaufania przekraczającą 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Pokazuj avatary tylko tych użytkowników Sone, którzy mają ilość punktów zaufania większą niż 0.
 Page.Options.Option.ShowAvatars.Always.Description=Zawsze pokazuj niestandardowe avatary. Uwaga: niektóre avatary mogą zawierać obraźliwe treści.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Tryb pracy
 Page.Options.Option.InsertionDelay.Description=Czas oczekiwania użytkownika Sone na modifikację profilu Sone przed jego załadowaniem.
 Page.Options.Option.PostsPerPage.Description=Ilość postów wyświetlanych na stronie przed pojawieniem się znaków paginacji.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=potem
 View.Time.AFewSecondsAgo=kilka sekund temu
 View.Time.HalfAMinuteAgo=około pół godziny temu
 View.Time.AMinuteAgo=około minutę temu
-View.Time.XMinutesAgo=${min}minut temu
+View.Time.XMinutesAgo={0} minut temu
 View.Time.HalfAnHourAgo=pół godziny temu
 View.Time.AnHourAgo=około godzinę temu
-View.Time.XHoursAgo=${hour} godzin temu
+View.Time.XHoursAgo={0} godzin temu
 View.Time.ADayAgo=około godzinę
-View.Time.XDaysAgo=${day} dni temu
+View.Time.XDaysAgo={0} dni temu
 View.Time.AWeekAgo=około tydzień temu
-View.Time.XWeeksAgo=${week} tygodni temu
+View.Time.XWeeksAgo={0} tygodni temu
 View.Time.AMonthAgo=około miesiąc temu
-View.Time.XMonthsAgo=${month} miesięcy
+View.Time.XMonthsAgo={0} miesięcy
 View.Time.AYearAgo=około rok temu
-View.Time.XYearsAgo=${year} lat temu
+View.Time.XYearsAgo={0} lat temu
 
 WebInterface.DefaultText.StatusUpdate=O czym teraz myślisz?
 WebInterface.DefaultText.Message=Napisz Wiadomość…
@@ -464,4 +471,4 @@ Notification.Mention.Text=Zostałeś oznaczony w następujących postach:
 Notification.SoneIsInserting.Text=Twoje Sone sone://{0} jest w tej chili wysyłane.
 Notification.SoneIsInserted.Text=Twoje sone://{0} zostało wysłane w {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Twoje Sone sone://{0} nie mogło zostać wysłane.
-# 458
+# 55-61, 465
index 85d82f1..bfc7301 100644 (file)
@@ -52,6 +52,13 @@ Page.Options.Option.ShowAvatars.Followed.Description=Показывать ава
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Показывать аватары только для Sone, которым вы вручную задали значение доверия выше 0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Показывать аватары только для Sone, значение доверия которых выше 0.
 Page.Options.Option.ShowAvatars.Always.Description=Всегда показывать пользовательские аватары. Предупреждение: некоторые аватары могут содержать раздражающие или оскорбительные изображения.
+Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
+Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
+Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
 Page.Options.Section.RuntimeOptions.Title=Поведение во время работы.
 Page.Options.Option.InsertionDelay.Description=Количество секунд, в течение которых выгрузчик Sone ожидает после изменения Sone до того, как он будет выгружен.
 Page.Options.Option.PostsPerPage.Description=Количество сообщений, которое должно быть показно на странице до того, как будут показаны кнопки переключения страниц.
@@ -387,18 +394,18 @@ View.Time.InTheFuture=в будущем
 View.Time.AFewSecondsAgo=несколько секунд назад
 View.Time.HalfAMinuteAgo=около полминуты назад
 View.Time.AMinuteAgo=около минуты назад
-View.Time.XMinutesAgo=${min} минут назад
+View.Time.XMinutesAgo={0} минут назад
 View.Time.HalfAnHourAgo=полчаса назад
 View.Time.AnHourAgo=около часа назад
-View.Time.XHoursAgo=${hour} часов назад
+View.Time.XHoursAgo={0} часов назад
 View.Time.ADayAgo=около дня назад
-View.Time.XDaysAgo=${day} дней назад
+View.Time.XDaysAgo={0} дней назад
 View.Time.AWeekAgo=около недели назад
-View.Time.XWeeksAgo=${week} недель назад
+View.Time.XWeeksAgo={0} недель назад
 View.Time.AMonthAgo=около месяца назад
-View.Time.XMonthsAgo=${month} месяцев назад
+View.Time.XMonthsAgo={0} месяцев назад
 View.Time.AYearAgo=около года назад
-View.Time.XYearsAgo=${year} лет назад
+View.Time.XYearsAgo={0} лет назад
 
 WebInterface.DefaultText.StatusUpdate=Что у вас на уме?
 WebInterface.DefaultText.Message=Написать сообщение…
@@ -464,4 +471,4 @@ Notification.Mention.Text=Вас упомянули в следующих соо
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-# 60, 100, 120-121, 308-310, 312-314, 458, 463-465
+# 55-61, 67, 107, 127-128, 315-317, 319-321, 465, 471-473
index 1767090..5ea5f64 100644 (file)
@@ -440,6 +440,50 @@ textarea {
        color: green;
 }
 
+#sone .post .linked-elements {
+       margin-top: 1ex;
+}
+
+#sone .linked-element.loaded .image {
+       display: inline-block;
+       border: solid 1px black;
+       width: 160px;
+       height: 120px;
+       background-size: cover;
+       background-position: center;
+       margin-right: 1ex;
+       margin-bottom: 1ex;
+}
+
+#sone .linked-element.loaded .html-page {
+       display: inline-block;
+       width: 160px;
+       max-height: 120px;
+       overflow: hidden;
+       vertical-align: top;
+}
+
+#sone .reply .linked-element.loaded .html-page {
+       width: 120px;
+       max-height: 90px;
+}
+
+#sone .linked-element.loaded .html-page .heading {
+       font-size: 115%;
+       font-weight: 600;
+       text-overflow: ellipsis;
+       overflow: hidden;
+       white-space: nowrap;
+}
+
+#sone .linked-element.loaded .html-page .description {
+       font-size: 85%;
+}
+
+#sone .linked-element.loaded .html-page {
+       display: inline-block;
+}
+
 #sone .post .replies {
        clear: both;
        padding-top: 0.2ex;
@@ -496,6 +540,21 @@ textarea {
        font-size: inherit;
 }
 
+#sone .post .reply .linked-elements {
+       margin-top: 1ex;
+}
+
+#sone .post .reply .linked-element.loaded .image {
+       display: inline-block;
+       border: solid 1px black;
+       width: 120px;
+       height: 90px;
+       background-size: cover;
+       background-position: center;
+       margin-right: 1ex;
+       margin-bottom: 1ex;
+}
+
 #sone .post .show-reply-form {
        display: inline;
 }
index 7ef585d..84be0d4 100644 (file)
@@ -1194,7 +1194,13 @@ function checkForRemovedReplies(oldNotification, newNotification) {
 }
 
 function getStatus() {
-       ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : isKnownSonesPage() ? {"soneIds": getShownSoneIds() } : {}, function(data, textStatus) {
+       var parameters = isViewSonePage() ? {"soneIds": getShownSoneId() } : isKnownSonesPage() ? {"soneIds": getShownSoneIds() } : {};
+       $.extend(parameters, {
+               "elements": JSON.stringify($(".linked-element.not-loaded").map(function () {
+                       return $(this).attr("title");
+               }).toArray())
+       });
+       ajaxGet("getStatus.ajax", parameters, function(data, textStatus) {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
@@ -1216,6 +1222,9 @@ function getStatus() {
                                        loadNewReply(value.id, value.sone, value.post, value.postSone);
                                });
                        }
+                       if (data.linkedElements) {
+                               loadLinkedElements(data.linkedElements)
+                       }
                        /* do it again in 5 seconds. */
                        setTimeout(getStatus, 5000);
                } else {
@@ -1520,6 +1529,41 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
        });
 }
 
+function loadLinkedElements(links) {
+       var failedElements = links.filter(function(element) {
+               return element.failed;
+       });
+       if (failedElements.length > 0) {
+               failedElements.forEach(function(element) {
+                       getLinkedElements(element.link).each(function() {
+                               $(this).remove()
+                       });
+               });
+       }
+       var loadedElements = links.filter(function(element) {
+               return !element.loading && !element.failed;
+       });
+       if (loadedElements.length > 0) {
+               ajaxGet("getLinkedElement.ajax", {
+                       "elements": JSON.stringify(loadedElements.map(function(element) {
+                               return element.link;
+                       }))
+               }, function (data, textStatus) {
+                       if ((data != null) && (data.success)) {
+                               data.linkedElements.forEach(function (linkedElement) {
+                                       getLinkedElements(linkedElement.link).each(function() {
+                                               $(this).replaceWith(linkedElement.html);
+                                       });
+                               });
+                       }
+               });
+       }
+}
+
+function getLinkedElements(link) {
+       return $(".linked-element[title='" + link + "']")
+}
+
 /**
  * Marks the given Sone as known if it is still new.
  *
@@ -1602,13 +1646,15 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updatePostTimes(postIds) {
-       ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       $.each(data.postTimes, function(index, value) {
-                               updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
-                       });
-               }
-       });
+       if (postIds != "") {
+        ajaxGet("getTimes.ajax", {"posts": postIds}, function (data, textStatus) {
+            if ((data != null) && data.success) {
+                $.each(data.postTimes, function (index, value) {
+                    updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
+                });
+            }
+        });
+    }
 }
 
 /**
@@ -1639,13 +1685,15 @@ function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updateReplyTimes(replyIds) {
-       ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       $.each(data.replyTimes, function(index, value) {
-                               updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
-                       });
-               }
-       });
+       if (replyIds != "") {
+        ajaxGet("getTimes.ajax", {"replies": replyIds}, function (data, textStatus) {
+            if ((data != null) && data.success) {
+                $.each(data.replyTimes, function (index, value) {
+                    updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
+                });
+            }
+        });
+    }
 }
 
 function resetActivity() {
index 644848a..b8b9cb2 100644 (file)
                                <%/foreach>
                        </div>
 
-                       <p id="description"><% album.description|parse sone=album.sone></p>
+                       <p id="description"><% album.description|parse sone=album.sone|render></p>
 
                        <%if album.sone.local>
                                <div class="show-edit-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
                                </div>
                        <%/if>
 
-                       <%foreach album.images image|paginate pageSize=core.preferences.imagesPerPage page=page>
+                       <%foreach album.images image|paginate pageSize=preferences.imagesPerPage page=page>
                                <%first>
                                        <h2><%= Page.ImageBrowser.Header.Images|l10n|html></h2>
                                        <%include include/pagination.html pageParameter=="page">
                                        </div>
                                        <div class="show-data">
                                                <div class="image-title"><% image.title|html></div>
-                                               <div class="image-description"><% image.description|parse sone=image.sone></div>
+                                               <div class="image-description"><% image.description|parse sone=image.sone|render></div>
                                        </div>
                                        <%if album.sone.local>
                                                <form class="edit-image" action="editImage.html" method="post">
                                <%/if>
                        </div>
 
-                       <p class="parsed"><%image.description|parse sone=image.sone></p>
+                       <p class="parsed"><%image.description|parse sone=image.sone|render></p>
 
                        <%if image.sone.local>
 
 
        <%elseif galleryRequested>
 
-               <%foreach albums album|paginate pageSize=core.preferences.imagesPerPage pageParameter=request.page pagination=albumPagination>
+               <%foreach albums album|paginate pageSize=preferences.imagesPerPage pageParameter=request.page pagination=albumPagination>
                        <%first>
                                <h2><%= Page.ImageBrowser.Header.Albums|l10n|html></h2>
                                <%include include/pagination.html pagination=albumPagination pageParameter=="page">
                                <div class="show-data">
                                        <div class="album-sone"><a href="imageBrowser.html?sone=<%album.sone.id|html>"><%album.sone.niceName|html></a></div>
                                        <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
-                                       <div class="album-description"><% album.description|parse sone=album.sone></div>
+                                       <div class="album-description"><% album.description|parse sone=album.sone|render></div>
                                </div>
                        </div>
                        <%= false|store key==endRow>
index 9aacad3..7c8c490 100644 (file)
@@ -14,7 +14,7 @@
                </div>
                <div class="show-data">
                        <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
-                       <div class="album-description"><% album.description|parse sone=album.sone></div>
+                       <div class="album-description"><% album.description|parse sone=album.sone|render></div>
                </div>
                <%if album.sone.local>
                        <form class="edit-album" action="editAlbum.html" method="post">
index 1c53d42..81683e4 100644 (file)
                                <%/if>
                        <%/if>
                        <% post.text|html|store key==originalText text==true>
-                       <% post.text|parse sone=post.sone|store key==parsedText text==true>
-                       <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
+                       <% post.text|parse sone=post.sone|store key==parsedText>
+                       <% parsedText|render|store key==renderedText text==true>
+                       <% parsedText|shorten length=preferences.charactersPerPost cut-off-length=preferences.postCutOffLength|render|store key==shortText text==true>
                        <div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
-                       <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
-                       <%if !shortText|match value=parsedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
-                       <%if !shortText|match value=parsedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
+                       <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=renderedText> hidden<%/if>"><% renderedText></div>
+                       <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match value=renderedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match value=renderedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
+                       <% parsedText|linked-elements sone=post.sone|store key==linkedElements>
+                       <% foreach linkedElements linkedElement>
+                               <%if !linkedElement.failed>
+                                       <% first>
+                                               <div class="linked-elements">
+                                       <%/first>
+                                       <% linkedElement|render-linked-element>
+                                       <% last>
+                                               </div>
+                                       <%/last>
+                               <%/if>
+                       <%/foreach>
                </div>
                <div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
                        <div class="bookmarks">
@@ -56,7 +69,7 @@
                        <div class="permalink permalink-post"><a href="post://<%post.id|html>">[<%= View.Post.Permalink|l10n|html>]</a></div>
                        <span class='separator'>·</span>
                        <div class="permalink permalink-author"><a href="sone://<%post.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
-                       <%if ! originalText|match value=parsedText>
+                       <%if ! originalText|match value=renderedText>
                                <span class='separator'>·</span>
                                <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
                        <%/if>
index 5e2ed7d..3a5e59a 100644 (file)
                <div>
                        <div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
                        <% reply.text|html|store key==originalText text==true>
-                       <% reply.text|parse sone=reply.sone|store key==parsedText text==true>
-                       <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
+                       <% reply.text|parse sone=reply.sone|store key==parsedText>
+                       <% parsedText|render|store key==renderedText text==true>
+                       <% parsedText|shorten length=preferences.charactersPerPost cut-off-length=preferences.postCutOffLength|render|store key==shortText text==true>
                        <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
-                       <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
-                       <%if !shortText|match value=parsedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.postId|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
-                       <%if !shortText|match value=parsedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
+                       <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=renderedText> hidden<%/if>"><% renderedText></div>
+                       <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match value=renderedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.postId|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match value=renderedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
+                       <% parsedText|linked-elements sone=reply.sone|store key==linkedElements>
+                       <% foreach linkedElements linkedElement>
+                               <%if !linkedElement.failed>
+                                       <% first>
+                                               <div class="linked-elements">
+                                       <%/first>
+                                       <% linkedElement|render-linked-element>
+                                       <% last>
+                                               </div>
+                                       <%/last>
+                               <%/if>
+                       <%/foreach>
                </div>
                <div class="reply-status-line status-line">
                        <div class="time"><% reply.time|date format=="MMM d, yyyy, HH:mm:ss"></div>
                        <span class='separator'>·</span>
                        <div class="permalink permalink-author"><a href="sone://<%reply.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
-                       <%if ! originalText|match value=parsedText>
+                       <%if ! originalText|match value=renderedText>
                                <span class='separator'>·</span>
                                <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
                        <%/if>
index 60b8ff7..455d81e 100644 (file)
@@ -5,7 +5,7 @@
        <div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
        <div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
        <div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
-       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format=="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
+       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format=="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|l10n|html></span></div>
        <div>
                <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
                <div class="sone-stats">(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size><%if ! sone.allImages.size|match value==0>, <%= View.Sone.Stats.Images|l10n 0=sone.allImages.size><%/if>)</div>
index 3d456d4..28fbec8 100644 (file)
@@ -72,6 +72,7 @@
                        <%/if>
                        <title><%album.title|xml></title>
                        <description><%album.description|xml></description>
+                       <!-- album-image is ignored, a random image is shown as album image. -->
                        <album-image><%album.albumImage.id|xml></album-image>
                        <%foreach album.images image>
                        <%first>
index 3ac37cf..922829c 100644 (file)
@@ -4,9 +4,9 @@
 
        <%foreach messages message>
                <%if message|substring start==0 length==1|match value=='!'>
-                       <p class="error"><% message|substring start==1|parse></p>
+                       <p class="error"><% message|substring start==1|parse|render></p>
                <%else>
-                       <p><% message|parse></p>
+                       <p><% message|parse|render></p>
                <%/if>
        <%foreachelse>
                <p><%= Page.Invalid.Text|l10n|html|replace needle=="{link}" replacement=='<a href="index.html">'|replace needle=="{/link}" replacement=='</a>'></p>
diff --git a/src/main/resources/templates/linked/html-page.html b/src/main/resources/templates/linked/html-page.html
new file mode 100644 (file)
index 0000000..e60fb9e
--- /dev/null
@@ -0,0 +1,6 @@
+<span class="linked-element loaded" title="<%link|html>">
+       <a class="html-page" href="/<% link|html>">
+               <div class="heading"><% title|html></div>
+               <div class="description"><% description|html></div>
+       </a>
+</span>
diff --git a/src/main/resources/templates/linked/image.html b/src/main/resources/templates/linked/image.html
new file mode 100644 (file)
index 0000000..d7fef5a
--- /dev/null
@@ -0,0 +1 @@
+<span class="linked-element loaded" title="<%link|html>"><a href="/<% link|html>"><span class="image" style="background-image: url('/<% link|html>')"></span></a></span>
diff --git a/src/main/resources/templates/linked/notLoaded.html b/src/main/resources/templates/linked/notLoaded.html
new file mode 100644 (file)
index 0000000..4a2d977
--- /dev/null
@@ -0,0 +1 @@
+<span class="linked-element not-loaded" title="<% link|html>"/>
index f35ecaf..fa9c53e 100644 (file)
@@ -1,4 +1,4 @@
-<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"></div>
+<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"|render></div>
 <%if disruptive>
        <div class="text"><%= Notification.NewVersion.Disruptive.Text|l10n|html|replace needle=="{em}" replacement=="<em>"|replace needle=="{/em}" replacement=="</em>"></div>
 <%/if>
index c15864f..8965e36 100644 (file)
@@ -1,7 +1,7 @@
 <%if soneStatus|match value=="inserting">
-       <%= Notification.SoneIsInserting.Text|l10n 0=insertSone.id|parse>
+       <%= Notification.SoneIsInserting.Text|l10n 0=insertSone.id|parse|render>
 <%elseif soneStatus|match value=="inserted">
-       <%= Notification.SoneIsInserted.Text|l10n 0=insertSone.id 1=insertDuration|parse>
+       <%= Notification.SoneIsInserted.Text|l10n 0=insertSone.id 1=insertDuration|parse|render>
 <%elseif soneStatus|match value=="insert-aborted">
-       <%= Notification.SoneInsertAborted.Text|l10n 0=insertSone.id|parse>
+       <%= Notification.SoneInsertAborted.Text|l10n 0=insertSone.id|parse|render>
 <%/if>
index 4ec88cb..34a1196 100644 (file)
                        </li>
                </ul>
 
+               <h2><%= Page.Options.Section.LoadLinkedImagesOptions.Title|l10n|html></h2>
+
+               <p><%= Page.Options.Option.LoadLinkedImages.Description|l10n|html></p>
+
+               <ul>
+                       <li>
+                               <input type="radio" name="load-linked-images" value="NEVER"<%if load-linked-images|match value==NEVER> checked="checked"<%/if>/>
+                               <%=Page.Options.Option.LoadLinkedImages.Never.Description|l10n|html>
+                       </li>
+                       <li>
+                               <input type="radio" name="load-linked-images" value="FOLLOWED"<%if load-linked-images|match value==FOLLOWED> checked="checked"<%/if>/>
+                               <%=Page.Options.Option.LoadLinkedImages.Followed.Description|l10n|html>
+                       </li>
+                       <li>
+                               <input type="radio" name="load-linked-images" value="MANUALLY_TRUSTED"<%if load-linked-images|match value==MANUALLY_TRUSTED> checked="checked"<%/if>/>
+                               <%=Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description|l10n|html>
+                       </li>
+                       <li>
+                               <input type="radio" name="load-linked-images" value="TRUSTED"<%if load-linked-images|match value==TRUSTED> checked="checked"<%/if>/>
+                               <%=Page.Options.Option.LoadLinkedImages.Trusted.Description|l10n|html>
+                       </li>
+                       <li>
+                               <input type="radio" name="load-linked-images" value="ALWAYS"<%if load-linked-images|match value==ALWAYS> checked="checked"<%/if>/>
+                               <%=Page.Options.Option.LoadLinkedImages.Always.Description|l10n|html>
+                       </li>
+               </ul>
+
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
index 4b88474..dd994ea 100644 (file)
@@ -52,7 +52,7 @@
                        <%foreach sone.profile.fields field>
                                <div class="profile-field">
                                        <div class="name"><% field.name|html></div>
-                                       <div class="value"><% field.value|parse sone=sone></div>
+                                       <div class="value"><% field.value|parse sone=sone|render></div>
                                </div>
                        <%/foreach>
 
diff --git a/src/test/java/net/pterodactylus/sone/Matchers.java b/src/test/java/net/pterodactylus/sone/Matchers.java
deleted file mode 100644 (file)
index 823cdf9..0000000
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Sone - Matchers.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;
-
-import static java.util.regex.Pattern.compile;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-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 com.google.common.base.Optional;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.hamcrest.TypeSafeDiagnosingMatcher;
-import org.hamcrest.TypeSafeMatcher;
-
-/**
- * Matchers used throughout the tests.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class Matchers {
-
-       public static Matcher<String> matchesRegex(final String regex) {
-               return new TypeSafeMatcher<String>() {
-                       @Override
-                       protected boolean matchesSafely(String item) {
-                               return compile(regex).matcher(item).matches();
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("matches: ").appendValue(regex);
-                       }
-               };
-       }
-
-       public static Matcher<InputStream> delivers(final byte[] data) {
-               return new TypeSafeMatcher<InputStream>() {
-                       byte[] readData = new byte[data.length];
-
-                       @Override
-                       protected boolean matchesSafely(InputStream inputStream) {
-                               int offset = 0;
-                               try {
-                                       while (true) {
-                                               int r = inputStream.read();
-                                               if (r == -1) {
-                                                       return offset == data.length;
-                                               }
-                                               if (offset == data.length) {
-                                                       return false;
-                                               }
-                                               if (data[offset] != (readData[offset] = (byte) r)) {
-                                                       return false;
-                                               }
-                                               offset++;
-                                       }
-                               } catch (IOException ioe1) {
-                                       return false;
-                               }
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendValue(data);
-                       }
-
-                       @Override
-                       protected void describeMismatchSafely(InputStream item,
-                                       Description mismatchDescription) {
-                               mismatchDescription.appendValue(readData);
-                       }
-               };
-       }
-
-       public static Matcher<Post> isPost(String postId, long time,
-                       String text, Optional<String> recipient) {
-               return new PostMatcher(postId, time, text, recipient);
-       }
-
-       public static Matcher<Post> isPostWithId(String postId) {
-               return new PostIdMatcher(postId);
-       }
-
-       public static Matcher<PostReply> isPostReply(String postReplyId,
-                       String postId, long time, String text) {
-               return new PostReplyMatcher(postReplyId, postId, time, text);
-       }
-
-       public static Matcher<Album> isAlbum(final String albumId,
-                       final String parentAlbumId,
-                       final String title, final String albumDescription) {
-               return new TypeSafeDiagnosingMatcher<Album>() {
-                       @Override
-                       protected boolean matchesSafely(Album album,
-                                       Description mismatchDescription) {
-                               if (!album.getId().equals(albumId)) {
-                                       mismatchDescription.appendText("ID is ")
-                                                       .appendValue(album.getId());
-                                       return false;
-                               }
-                               if (parentAlbumId == null) {
-                                       if (album.getParent() != null) {
-                                               mismatchDescription.appendText("has parent album");
-                                               return false;
-                                       }
-                               } else {
-                                       if (album.getParent() == null) {
-                                               mismatchDescription.appendText("has no parent album");
-                                               return false;
-                                       }
-                                       if (!album.getParent().getId().equals(parentAlbumId)) {
-                                               mismatchDescription.appendText("parent album is ")
-                                                               .appendValue(album.getParent().getId());
-                                               return false;
-                                       }
-                               }
-                               if (!title.equals(album.getTitle())) {
-                                       mismatchDescription.appendText("has title ")
-                                                       .appendValue(album.getTitle());
-                                       return false;
-                               }
-                               if (!albumDescription.equals(album.getDescription())) {
-                                       mismatchDescription.appendText("has description ")
-                                                       .appendValue(album.getDescription());
-                                       return false;
-                               }
-                               return true;
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("is album ").appendValue(albumId);
-                               if (parentAlbumId == null) {
-                                       description.appendText(", has no parent");
-                               } else {
-                                       description.appendText(", has parent ")
-                                                       .appendValue(parentAlbumId);
-                               }
-                               description.appendText(", has title ").appendValue(title);
-                               description.appendText(", has description ")
-                                               .appendValue(albumDescription);
-                       }
-               };
-       }
-
-       public static Matcher<Image> isImage(final String id,
-                       final long creationTime,
-                       final String key, final String title,
-                       final String imageDescription,
-                       final int width, final int height) {
-               return new TypeSafeDiagnosingMatcher<Image>() {
-                       @Override
-                       protected boolean matchesSafely(Image image,
-                                       Description mismatchDescription) {
-                               if (!image.getId().equals(id)) {
-                                       mismatchDescription.appendText("ID is ")
-                                                       .appendValue(image.getId());
-                                       return false;
-                               }
-                               if (image.getCreationTime() != creationTime) {
-                                       mismatchDescription.appendText("created at @")
-                                                       .appendValue(image.getCreationTime());
-                                       return false;
-                               }
-                               if (!image.getKey().equals(key)) {
-                                       mismatchDescription.appendText("key is ")
-                                                       .appendValue(image.getKey());
-                                       return false;
-                               }
-                               if (!image.getTitle().equals(title)) {
-                                       mismatchDescription.appendText("title is ")
-                                                       .appendValue(image.getTitle());
-                                       return false;
-                               }
-                               if (!image.getDescription().equals(imageDescription)) {
-                                       mismatchDescription.appendText("description is ")
-                                                       .appendValue(image.getDescription());
-                                       return false;
-                               }
-                               if (image.getWidth() != width) {
-                                       mismatchDescription.appendText("width is ")
-                                                       .appendValue(image.getWidth());
-                                       return false;
-                               }
-                               if (image.getHeight() != height) {
-                                       mismatchDescription.appendText("height is ")
-                                                       .appendValue(image.getHeight());
-                                       return false;
-                               }
-                               return true;
-                       }
-
-                       @Override
-                       public void describeTo(Description description) {
-                               description.appendText("image with ID ").appendValue(id);
-                               description.appendText(", created at @")
-                                               .appendValue(creationTime);
-                               description.appendText(", has key ").appendValue(key);
-                               description.appendText(", has title ").appendValue(title);
-                               description.appendText(", has description ")
-                                               .appendValue(imageDescription);
-                               description.appendText(", has width ").appendValue(width);
-                               description.appendText(", has height ").appendValue(height);
-                       }
-               };
-       }
-
-       private static class PostMatcher extends TypeSafeDiagnosingMatcher<Post> {
-
-               private final String postId;
-               private final long time;
-               private final String text;
-               private final Optional<String> recipient;
-
-               private PostMatcher(String postId, long time, String text,
-                               Optional<String> recipient) {
-                       this.postId = postId;
-                       this.time = time;
-                       this.text = text;
-                       this.recipient = recipient;
-               }
-
-               @Override
-               protected boolean matchesSafely(Post post,
-                               Description mismatchDescription) {
-                       if (!post.getId().equals(postId)) {
-                               mismatchDescription.appendText("ID is not ")
-                                               .appendValue(postId);
-                               return false;
-                       }
-                       if (post.getTime() != time) {
-                               mismatchDescription.appendText("Time is not @")
-                                               .appendValue(time);
-                               return false;
-                       }
-                       if (!post.getText().equals(text)) {
-                               mismatchDescription.appendText("Text is not ")
-                                               .appendValue(text);
-                               return false;
-                       }
-                       if (recipient.isPresent()) {
-                               if (!post.getRecipientId().isPresent()) {
-                                       mismatchDescription.appendText(
-                                                       "Recipient not present");
-                                       return false;
-                               }
-                               if (!post.getRecipientId().get().equals(recipient.get())) {
-                                       mismatchDescription.appendText("Recipient is not ")
-                                                       .appendValue(recipient.get());
-                                       return false;
-                               }
-                       } else {
-                               if (post.getRecipientId().isPresent()) {
-                                       mismatchDescription.appendText("Recipient is present");
-                                       return false;
-                               }
-                       }
-                       return true;
-               }
-
-               @Override
-               public void describeTo(Description description) {
-                       description.appendText("is post with ID ")
-                                       .appendValue(postId);
-                       description.appendText(", created at @").appendValue(time);
-                       description.appendText(", text ").appendValue(text);
-                       if (recipient.isPresent()) {
-                               description.appendText(", directed at ")
-                                               .appendValue(recipient.get());
-                       }
-               }
-
-       }
-
-       private static class PostIdMatcher extends TypeSafeDiagnosingMatcher<Post> {
-
-               private final String id;
-
-               private PostIdMatcher(String id) {
-                       this.id = id;
-               }
-
-               @Override
-               protected boolean matchesSafely(Post item,
-                               Description mismatchDescription) {
-                       if (!item.getId().equals(id)) {
-                               mismatchDescription.appendText("post has ID ").appendValue(item.getId());
-                               return false;
-                       }
-                       return true;
-               }
-
-               @Override
-               public void describeTo(Description description) {
-                       description.appendText("post with ID ").appendValue(id);
-               }
-
-       }
-
-       private static class PostReplyMatcher
-                       extends TypeSafeDiagnosingMatcher<PostReply> {
-
-               private final String postReplyId;
-               private final String postId;
-               private final long time;
-               private final String text;
-
-               private PostReplyMatcher(String postReplyId, String postId, long time,
-                               String text) {
-                       this.postReplyId = postReplyId;
-                       this.postId = postId;
-                       this.time = time;
-                       this.text = text;
-               }
-
-               @Override
-               protected boolean matchesSafely(PostReply postReply,
-                               Description mismatchDescription) {
-                       if (!postReply.getId().equals(postReplyId)) {
-                               mismatchDescription.appendText("is post reply ")
-                                               .appendValue(postReply.getId());
-                               return false;
-                       }
-                       if (!postReply.getPostId().equals(postId)) {
-                               mismatchDescription.appendText("is reply to ")
-                                               .appendValue(postReply.getPostId());
-                               return false;
-                       }
-                       if (postReply.getTime() != time) {
-                               mismatchDescription.appendText("is created at @").appendValue(
-                                               postReply.getTime());
-                               return false;
-                       }
-                       if (!postReply.getText().equals(text)) {
-                               mismatchDescription.appendText("says ")
-                                               .appendValue(postReply.getText());
-                               return false;
-                       }
-                       return true;
-               }
-
-               @Override
-               public void describeTo(Description description) {
-                       description.appendText("is post reply ").appendValue(postReplyId);
-                       description.appendText(", replies to post ").appendValue(postId);
-                       description.appendText(", is created at @").appendValue(time);
-                       description.appendText(", says ").appendValue(text);
-               }
-
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java b/src/test/java/net/pterodactylus/sone/TestAlbumBuilder.java
deleted file mode 100644 (file)
index ea21118..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-package net.pterodactylus.sone;
-
-import static java.util.UUID.randomUUID;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.Album.Modifier;
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.AlbumBuilder;
-
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * {@link AlbumBuilder} that returns a mocked {@link Album}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestAlbumBuilder implements AlbumBuilder {
-
-       private final Album album = mock(Album.class);
-       private final List<Album> albums = new ArrayList<Album>();
-       private final List<Image> images = new ArrayList<Image>();
-       private Album parentAlbum;
-       private String title;
-       private String description;
-       private String imageId;
-
-       public TestAlbumBuilder() {
-               when(album.getTitle()).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) {
-                               return title;
-                       }
-               });
-               when(album.getDescription()).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) {
-                               return description;
-                       }
-               });
-               when(album.getAlbums()).thenReturn(albums);
-               when(album.getImages()).thenReturn(images);
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) {
-                               albums.add((Album) invocation.getArguments()[0]);
-                               ((Album) invocation.getArguments()[0]).setParent(album);
-                               return null;
-                       }
-               }).when(album).addAlbum(any(Album.class));
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) {
-                               images.add((Image) invocation.getArguments()[0]);
-                               return null;
-                       }
-               }).when(album).addImage(any(Image.class));
-               doAnswer(new Answer<Void>() {
-                       @Override
-                       public Void answer(InvocationOnMock invocation) {
-                               parentAlbum = (Album) invocation.getArguments()[0];
-                               return null;
-                       }
-               }).when(album).setParent(any(Album.class));
-               when(album.getParent()).thenAnswer(new Answer<Album>() {
-                       @Override
-                       public Album answer(InvocationOnMock invocation) {
-                               return parentAlbum;
-                       }
-               });
-               when(album.modify()).thenReturn(new Modifier() {
-                       @Override
-                       public Modifier setTitle(String title) {
-                               TestAlbumBuilder.this.title = title;
-                               return this;
-                       }
-
-                       @Override
-                       public Modifier setDescription(String description) {
-                               TestAlbumBuilder.this.description = description;
-                               return this;
-                       }
-
-                       @Override
-                       public Album update() throws IllegalStateException {
-                               return album;
-                       }
-               });
-       }
-
-       @Override
-       public AlbumBuilder randomId() {
-               when(album.getId()).thenReturn(randomUUID().toString());
-               return this;
-       }
-
-       @Override
-       public AlbumBuilder withId(String id) {
-               when(album.getId()).thenReturn(id);
-               return this;
-       }
-
-       @Override
-       public AlbumBuilder by(Sone sone) {
-               when(album.getSone()).thenReturn(sone);
-               return this;
-       }
-
-       @Override
-       public Album build() throws IllegalStateException {
-               return album;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestImageBuilder.java b/src/test/java/net/pterodactylus/sone/TestImageBuilder.java
deleted file mode 100644 (file)
index 48a8fad..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-package net.pterodactylus.sone;
-
-import static java.util.UUID.randomUUID;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.ImageBuilder;
-
-/**
- * {@link ImageBuilder} implementation that returns a mocked {@link Image}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestImageBuilder implements ImageBuilder {
-
-       private final Image image;
-
-       public TestImageBuilder() {
-               image = mock(Image.class);
-               Image.Modifier imageModifier = new Image.Modifier() {
-                       private Sone sone = image.getSone();
-                       private long creationTime = image.getCreationTime();
-                       private String key = image.getKey();
-                       private String title = image.getTitle();
-                       private String description = image.getDescription();
-                       private int width = image.getWidth();
-                       private int height = image.getHeight();
-
-                       @Override
-                       public Image.Modifier setSone(Sone sone) {
-                               this.sone = sone;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setCreationTime(long creationTime) {
-                               this.creationTime = creationTime;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setKey(String key) {
-                               this.key = key;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setTitle(String title) {
-                               this.title = title;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setDescription(String description) {
-                               this.description = description;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setWidth(int width) {
-                               this.width = width;
-                               return this;
-                       }
-
-                       @Override
-                       public Image.Modifier setHeight(int height) {
-                               this.height = height;
-                               return this;
-                       }
-
-                       @Override
-                       public Image update() throws IllegalStateException {
-                               when(image.getSone()).thenReturn(sone);
-                               when(image.getCreationTime()).thenReturn(creationTime);
-                               when(image.getKey()).thenReturn(key);
-                               when(image.getTitle()).thenReturn(title);
-                               when(image.getDescription()).thenReturn(description);
-                               when(image.getWidth()).thenReturn(width);
-                               when(image.getHeight()).thenReturn(height);
-                               return image;
-                       }
-               };
-               when(image.modify()).thenReturn(imageModifier);
-       }
-
-       @Override
-       public ImageBuilder randomId() {
-               when(image.getId()).thenReturn(randomUUID().toString());
-               return this;
-       }
-
-       @Override
-       public ImageBuilder withId(String id) {
-               when(image.getId()).thenReturn(id);
-               return this;
-       }
-
-       @Override
-       public Image build() throws IllegalStateException {
-               return image;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestPostBuilder.java b/src/test/java/net/pterodactylus/sone/TestPostBuilder.java
deleted file mode 100644 (file)
index 29c4c40..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package net.pterodactylus.sone;
-
-import static com.google.common.base.Optional.fromNullable;
-import static java.lang.System.currentTimeMillis;
-import static java.util.UUID.randomUUID;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.PostBuilder;
-
-/**
- * {@link PostBuilder} implementation that returns a mocked {@link Post}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestPostBuilder implements PostBuilder {
-
-       private final Post post = mock(Post.class);
-       private String recipientId = null;
-
-       @Override
-       public PostBuilder copyPost(Post post) throws NullPointerException {
-               return this;
-       }
-
-       @Override
-       public PostBuilder from(String senderId) {
-               final Sone sone = mock(Sone.class);
-               when(sone.getId()).thenReturn(senderId);
-               when(post.getSone()).thenReturn(sone);
-               return this;
-       }
-
-       @Override
-       public PostBuilder randomId() {
-               when(post.getId()).thenReturn(randomUUID().toString());
-               return this;
-       }
-
-       @Override
-       public PostBuilder withId(String id) {
-               when(post.getId()).thenReturn(id);
-               return this;
-       }
-
-       @Override
-       public PostBuilder currentTime() {
-               when(post.getTime()).thenReturn(currentTimeMillis());
-               return this;
-       }
-
-       @Override
-       public PostBuilder withTime(long time) {
-               when(post.getTime()).thenReturn(time);
-               return this;
-       }
-
-       @Override
-       public PostBuilder withText(String text) {
-               when(post.getText()).thenReturn(text);
-               return this;
-       }
-
-       @Override
-       public PostBuilder to(String recipientId) {
-               this.recipientId = recipientId;
-               return this;
-       }
-
-       @Override
-       public Post build() throws IllegalStateException {
-               when(post.getRecipientId()).thenReturn(fromNullable(recipientId));
-               return post;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java b/src/test/java/net/pterodactylus/sone/TestPostReplyBuilder.java
deleted file mode 100644 (file)
index 6b929c7..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-package net.pterodactylus.sone;
-
-import static java.lang.System.currentTimeMillis;
-import static java.util.UUID.randomUUID;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.database.PostReplyBuilder;
-
-/**
- * {@link PostReplyBuilder} that returns a mocked {@link PostReply}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestPostReplyBuilder implements PostReplyBuilder {
-
-       private final PostReply postReply = mock(PostReply.class);
-
-       @Override
-       public PostReplyBuilder to(String postId) {
-               when(postReply.getPostId()).thenReturn(postId);
-               return this;
-       }
-
-       @Override
-       public PostReply build() throws IllegalStateException {
-               return postReply;
-       }
-
-       @Override
-       public PostReplyBuilder randomId() {
-               when(postReply.getId()).thenReturn(randomUUID().toString());
-               return this;
-       }
-
-       @Override
-       public PostReplyBuilder withId(String id) {
-               when(postReply.getId()).thenReturn(id);
-               return this;
-       }
-
-       @Override
-       public PostReplyBuilder from(String senderId) {
-               Sone sone = mock(Sone.class);
-               when(sone.getId()).thenReturn(senderId);
-               when(postReply.getSone()).thenReturn(sone);
-               return this;
-       }
-
-       @Override
-       public PostReplyBuilder currentTime() {
-               when(postReply.getTime()).thenReturn(currentTimeMillis());
-               return this;
-       }
-
-       @Override
-       public PostReplyBuilder withTime(long time) {
-               when(postReply.getTime()).thenReturn(time);
-               return this;
-       }
-
-       @Override
-       public PostReplyBuilder withText(String text) {
-               when(postReply.getText()).thenReturn(text);
-               return this;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestUtil.java b/src/test/java/net/pterodactylus/sone/TestUtil.java
deleted file mode 100644 (file)
index fa7879d..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.pterodactylus.sone;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-
-/**
- * Utilities for testing.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestUtil {
-
-       public static void setFinalField(Object object, String fieldName, Object value) {
-               try {
-                       Field clientCoreField = object.getClass().getField(fieldName);
-                       clientCoreField.setAccessible(true);
-                       Field modifiersField = Field.class.getDeclaredField("modifiers");
-                       modifiersField.setAccessible(true);
-                       modifiersField.setInt(clientCoreField, clientCoreField.getModifiers() & ~Modifier.FINAL);
-                       clientCoreField.set(object, value);
-               } catch (NoSuchFieldException e) {
-                       throw new RuntimeException(e);
-               } catch (IllegalAccessException e) {
-                       throw new RuntimeException(e);
-               }
-       }
-
-       public static <T> T getPrivateField(Object object, String fieldName) {
-               try {
-                       Field field = object.getClass().getDeclaredField(fieldName);
-                       field.setAccessible(true);
-                       return (T) field.get(object);
-               } catch (NoSuchFieldException e) {
-                       throw new RuntimeException(e);
-               } catch (IllegalAccessException e) {
-                       throw new RuntimeException(e);
-               }
-       }
-
-       public static <T> T callPrivateMethod(Object object, String methodName) {
-               try {
-                       Method method = object.getClass().getDeclaredMethod(methodName, new Class[0]);
-                       method.setAccessible(true);
-                       return (T) method.invoke(object);
-               } catch (NoSuchMethodException e) {
-                       throw new RuntimeException(e);
-               } catch (InvocationTargetException e) {
-                       throw new RuntimeException(e);
-               } catch (IllegalAccessException e) {
-                       throw new RuntimeException(e);
-               }
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/TestValue.java b/src/test/java/net/pterodactylus/sone/TestValue.java
deleted file mode 100644 (file)
index 43cf0a6..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-package net.pterodactylus.sone;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-import net.pterodactylus.util.config.ConfigurationException;
-import net.pterodactylus.util.config.Value;
-
-import com.google.common.base.Objects;
-
-/**
- * Simple {@link Value} implementation.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TestValue<T> implements Value<T> {
-
-       private final AtomicReference<T> value = new AtomicReference<T>();
-
-       public TestValue(T originalValue) {
-               value.set(originalValue);
-       }
-
-       @Override
-       public T getValue() throws ConfigurationException {
-               return value.get();
-       }
-
-       @Override
-       public T getValue(T defaultValue) {
-               final T realValue = value.get();
-               return (realValue != null) ? realValue : defaultValue;
-       }
-
-       @Override
-       public void setValue(T newValue) throws ConfigurationException {
-               value.set(newValue);
-       }
-
-       @Override
-       public int hashCode() {
-               return value.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               return (obj instanceof TestValue) && Objects.equal(value.get(),
-                               ((TestValue) obj).value.get());
-       }
-
-       @Override
-       public String toString() {
-               return String.valueOf(value.get());
-       }
-
-       public static <T> Value<T> from(T value) {
-               return new TestValue<T>(value);
-       }
-
-}
index 7deb805..4a26203 100644 (file)
@@ -1,10 +1,10 @@
 package net.pterodactylus.sone.core;
 
 import static com.google.common.base.Optional.of;
-import static net.pterodactylus.sone.Matchers.isAlbum;
-import static net.pterodactylus.sone.Matchers.isImage;
-import static net.pterodactylus.sone.Matchers.isPost;
-import static net.pterodactylus.sone.Matchers.isPostReply;
+import static net.pterodactylus.sone.test.Matchers.isAlbum;
+import static net.pterodactylus.sone.test.Matchers.isImage;
+import static net.pterodactylus.sone.test.Matchers.isPost;
+import static net.pterodactylus.sone.test.Matchers.isPostReply;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -23,11 +23,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import net.pterodactylus.sone.TestAlbumBuilder;
-import net.pterodactylus.sone.TestImageBuilder;
-import net.pterodactylus.sone.TestPostBuilder;
-import net.pterodactylus.sone.TestPostReplyBuilder;
-import net.pterodactylus.sone.TestValue;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
@@ -47,6 +42,11 @@ import net.pterodactylus.sone.database.PostBuilder;
 import net.pterodactylus.sone.database.PostBuilderFactory;
 import net.pterodactylus.sone.database.PostReplyBuilder;
 import net.pterodactylus.sone.database.PostReplyBuilderFactory;
+import net.pterodactylus.sone.test.TestAlbumBuilder;
+import net.pterodactylus.sone.test.TestImageBuilder;
+import net.pterodactylus.sone.test.TestPostBuilder;
+import net.pterodactylus.sone.test.TestPostReplyBuilder;
+import net.pterodactylus.sone.test.TestValue;
 import net.pterodactylus.util.config.Configuration;
 
 import com.google.common.base.Optional;
index 8211c5e..2ebc428 100644 (file)
@@ -1,10 +1,11 @@
 package net.pterodactylus.sone.core;
 
+import static freenet.client.FetchException.FetchExceptionMode.ALL_DATA_NOT_FOUND;
 import static freenet.keys.InsertableClientSSK.createRandom;
 import static freenet.node.RequestStarter.INTERACTIVE_PRIORITY_CLASS;
 import static freenet.node.RequestStarter.PREFETCH_PRIORITY_CLASS;
-import static net.pterodactylus.sone.Matchers.delivers;
-import static net.pterodactylus.sone.TestUtil.setFinalField;
+import static net.pterodactylus.sone.test.Matchers.delivers;
+import static net.pterodactylus.sone.test.TestUtil.setFinalField;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
@@ -12,6 +13,7 @@ import static org.hamcrest.Matchers.nullValue;
 import static org.mockito.ArgumentCaptor.forClass;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyShort;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -19,6 +21,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
@@ -26,7 +29,7 @@ import java.io.IOException;
 import java.net.MalformedURLException;
 import java.util.HashMap;
 
-import net.pterodactylus.sone.TestUtil;
+import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback;
 import net.pterodactylus.sone.core.FreenetInterface.Callback;
 import net.pterodactylus.sone.core.FreenetInterface.Fetched;
 import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
@@ -36,11 +39,13 @@ import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
 import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.data.impl.ImageImpl;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.data.impl.ImageImpl;
+import net.pterodactylus.sone.test.TestUtil;
 
 import freenet.client.ClientMetadata;
+import freenet.client.FetchContext;
 import freenet.client.FetchException;
 import freenet.client.FetchException.FetchExceptionMode;
 import freenet.client.FetchResult;
@@ -49,7 +54,12 @@ import freenet.client.InsertBlock;
 import freenet.client.InsertContext;
 import freenet.client.InsertException;
 import freenet.client.InsertException.InsertExceptionMode;
+import freenet.client.Metadata;
+import freenet.client.async.ClientContext;
+import freenet.client.async.ClientGetCallback;
+import freenet.client.async.ClientGetter;
 import freenet.client.async.ClientPutter;
+import freenet.client.async.SnoopMetadata;
 import freenet.client.async.USKCallback;
 import freenet.client.async.USKManager;
 import freenet.crypt.DummyRandomSource;
@@ -90,6 +100,20 @@ public class FreenetInterfaceTest {
        private final Image image = mock(Image.class);
        private InsertToken insertToken;
        private final Bucket bucket = mock(Bucket.class);
+       private final ArgumentCaptor<ClientGetCallback> clientGetCallback = forClass(ClientGetCallback.class);
+       private final FreenetURI uri = new FreenetURI("KSK@pgl.png");
+       private final FetchResult fetchResult = mock(FetchResult.class);
+       private final BackgroundFetchCallback backgroundFetchCallback = mock(BackgroundFetchCallback.class);
+       private final ClientGetter clientGetter = mock(ClientGetter.class);
+
+       public FreenetInterfaceTest() throws MalformedURLException {
+       }
+
+       @Before
+       public void setupHighLevelSimpleClient() throws Exception {
+               when(highLevelSimpleClient.getFetchContext()).thenReturn(mock(FetchContext.class));
+               when(highLevelSimpleClient.fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort())).thenReturn( clientGetter);
+       }
 
        @Before
        public void setupFreenetInterface() {
@@ -97,6 +121,7 @@ public class FreenetInterfaceTest {
                setFinalField(node, "clientCore", nodeClientCore);
                setFinalField(node, "random", randomSource);
                setFinalField(nodeClientCore, "uskManager", uskManager);
+               setFinalField(nodeClientCore, "clientContext", mock(ClientContext.class));
                freenetInterface = new FreenetInterface(eventBus, node);
                insertToken = freenetInterface.new InsertToken(image);
                insertToken.setBucket(bucket);
@@ -141,7 +166,7 @@ public class FreenetInterfaceTest {
        @Test
        public void fetchReturnsNullOnFetchExceptions() throws MalformedURLException, FetchException {
                FreenetURI freenetUri = new FreenetURI("KSK@GPLv2.txt");
-               FetchException fetchException = new FetchException(FetchExceptionMode.ALL_DATA_NOT_FOUND);
+               FetchException fetchException = new FetchException(ALL_DATA_NOT_FOUND);
                when(highLevelSimpleClient.fetch(freenetUri)).thenThrow(fetchException);
                Fetched fetched = freenetInterface.fetchUri(freenetUri);
                assertThat(fetched, nullValue());
@@ -246,7 +271,7 @@ public class FreenetInterfaceTest {
        public void registeringAnActiveNonUskWillNotSubscribeToAUsk()
        throws MalformedURLException {
                FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
-           freenetInterface.registerActiveUsk(freenetUri, null);
+               freenetInterface.registerActiveUsk(freenetUri, null);
                verify(uskManager, never()).subscribe(any(USK.class),
                                any(USKCallback.class), anyBoolean(),
                                eq((RequestClient) highLevelSimpleClient));
@@ -256,7 +281,7 @@ public class FreenetInterfaceTest {
        public void registeringAnInactiveNonUskWillNotSubscribeToAUsk()
        throws MalformedURLException {
                FreenetURI freenetUri = createRandom(randomSource, "test-0").getURI();
-           freenetInterface.registerPassiveUsk(freenetUri, null);
+               freenetInterface.registerPassiveUsk(freenetUri, null);
                verify(uskManager, never()).subscribe(any(USK.class),
                                any(USKCallback.class), anyBoolean(),
                                eq((RequestClient) highLevelSimpleClient));
@@ -399,4 +424,91 @@ public class FreenetInterfaceTest {
                assertThat(insertTokenSupplier.apply(image), notNullValue());
        }
 
+       @Test
+       public void backgroundFetchCanBeStarted() throws Exception {
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), any(ClientGetCallback.class), any(FetchContext.class), anyShort());
+       }
+
+       @Test
+       public void backgroundFetchRegistersSnoopAndRestartsTheRequest() throws Exception {
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               verify(clientGetter).setMetaSnoop(any(SnoopMetadata.class));
+               verify(clientGetter).restart(eq(uri), anyBoolean(), any(ClientContext.class));
+       }
+
+       @Test
+       public void requestIsNotCancelledForImageMimeType() {
+               verifySnoopCancelsRequestForMimeType("image/png", false);
+               verify(backgroundFetchCallback, never()).failed(uri);
+       }
+
+       @Test
+       public void requestIsCancelledForNullMimeType() {
+               verifySnoopCancelsRequestForMimeType(null, true);
+               verify(backgroundFetchCallback, never()).shouldCancel(eq(uri), ArgumentMatchers.<String>any(), anyLong());
+               verify(backgroundFetchCallback).failed(uri);
+       }
+
+       @Test
+       public void requestIsCancelledForVideoMimeType() {
+               verifySnoopCancelsRequestForMimeType("video/mkv", true);
+               verify(backgroundFetchCallback).failed(uri);
+       }
+
+       @Test
+       public void requestIsCancelledForAudioMimeType() {
+               verifySnoopCancelsRequestForMimeType("audio/mpeg", true);
+               verify(backgroundFetchCallback).failed(uri);
+       }
+
+       @Test
+       public void requestIsCancelledForTextMimeType() {
+               verifySnoopCancelsRequestForMimeType("text/plain", true);
+               verify(backgroundFetchCallback).failed(uri);
+       }
+
+       private void verifySnoopCancelsRequestForMimeType(String mimeType, boolean cancel) {
+               when(backgroundFetchCallback.shouldCancel(eq(uri), eq(mimeType), anyLong())).thenReturn(cancel);
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               ArgumentCaptor<SnoopMetadata> snoopMetadata = forClass(SnoopMetadata.class);
+               verify(clientGetter).setMetaSnoop(snoopMetadata.capture());
+               Metadata metadata = mock(Metadata.class);
+               when(metadata.getMIMEType()).thenReturn(mimeType);
+               assertThat(snoopMetadata.getValue().snoopMetadata(metadata, mock(ClientContext.class)), is(cancel));
+       }
+
+       @Test
+       public void callbackOfBackgroundFetchIsNotifiedOnSuccess() throws Exception {
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
+               when(fetchResult.getMimeType()).thenReturn("image/png");
+               when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 });
+               clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class));
+               verify(backgroundFetchCallback).loaded(uri, "image/png", new byte[] { 1, 2, 3, 4, 5 });
+               verifyNoMoreInteractions(backgroundFetchCallback);
+       }
+
+       @Test
+       public void callbackOfBackgroundFetchIsNotifiedOnFailure() throws Exception {
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
+               when(fetchResult.getMimeType()).thenReturn("image/png");
+               when(fetchResult.asByteArray()).thenReturn(new byte[] { 1, 2, 3, 4, 5 });
+               clientGetCallback.getValue().onFailure(new FetchException(ALL_DATA_NOT_FOUND), mock(ClientGetter.class));
+               verify(backgroundFetchCallback).failed(uri);
+               verifyNoMoreInteractions(backgroundFetchCallback);
+       }
+
+       @Test
+       public void callbackOfBackgroundFetchIsNotifiedAsFailureIfBucketCanNotBeLoaded() throws Exception {
+               freenetInterface.startFetch(uri, backgroundFetchCallback);
+               verify(highLevelSimpleClient).fetch(eq(uri), anyLong(), clientGetCallback.capture(), any(FetchContext.class), anyShort());
+               when(fetchResult.getMimeType()).thenReturn("image/png");
+               when(fetchResult.asByteArray()).thenThrow(IOException.class);
+               clientGetCallback.getValue().onSuccess(fetchResult, mock(ClientGetter.class));
+               verify(backgroundFetchCallback).failed(uri);
+               verifyNoMoreInteractions(backgroundFetchCallback);
+       }
+
 }
index e12fe93..49777f0 100644 (file)
@@ -7,7 +7,7 @@ import static org.hamcrest.Matchers.not;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import net.pterodactylus.sone.TestValue;
+import net.pterodactylus.sone.test.TestValue;
 import net.pterodactylus.util.config.Configuration;
 
 import com.google.common.eventbus.EventBus;
index 47e26b7..8dfce3d 100644 (file)
@@ -10,7 +10,7 @@ import static org.mockito.Mockito.when;
 import java.util.HashSet;
 import java.util.Set;
 
-import net.pterodactylus.sone.TestValue;
+import net.pterodactylus.sone.test.TestValue;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.config.Value;
index c16ce9d..e6bfffe 100644 (file)
@@ -1,7 +1,7 @@
 package net.pterodactylus.sone.database.memory;
 
 import static com.google.common.base.Optional.fromNullable;
-import static net.pterodactylus.sone.Matchers.isPostWithId;
+import static net.pterodactylus.sone.test.Matchers.isPostWithId;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.is;
index d305031..352b3b2 100644 (file)
@@ -20,10 +20,10 @@ package net.pterodactylus.sone.database.memory;
 import static com.google.common.base.Optional.of;
 import static java.util.Arrays.asList;
 import static java.util.UUID.randomUUID;
-import static net.pterodactylus.sone.Matchers.isAlbum;
-import static net.pterodactylus.sone.Matchers.isImage;
-import static net.pterodactylus.sone.Matchers.isPost;
-import static net.pterodactylus.sone.Matchers.isPostReply;
+import static net.pterodactylus.sone.test.Matchers.isAlbum;
+import static net.pterodactylus.sone.test.Matchers.isImage;
+import static net.pterodactylus.sone.test.Matchers.isPost;
+import static net.pterodactylus.sone.test.Matchers.isPostReply;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
@@ -44,17 +44,17 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import net.pterodactylus.sone.TestAlbumBuilder;
-import net.pterodactylus.sone.TestImageBuilder;
-import net.pterodactylus.sone.TestPostBuilder;
-import net.pterodactylus.sone.TestPostReplyBuilder;
-import net.pterodactylus.sone.TestValue;
 import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.data.impl.AlbumImpl;
 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.impl.AlbumImpl;
+import net.pterodactylus.sone.test.TestAlbumBuilder;
+import net.pterodactylus.sone.test.TestImageBuilder;
+import net.pterodactylus.sone.test.TestPostBuilder;
+import net.pterodactylus.sone.test.TestPostReplyBuilder;
+import net.pterodactylus.sone.test.TestValue;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.Value;
 
diff --git a/src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java b/src/test/java/net/pterodactylus/sone/fcp/FcpInterfaceTest.java
deleted file mode 100644 (file)
index 2312907..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-package net.pterodactylus.sone.fcp;
-
-import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS;
-import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO;
-import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent;
-import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent;
-import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link FcpInterface} and its subclasses.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FcpInterfaceTest {
-
-       private final FcpInterface fcpInterface = new FcpInterface(null);
-
-       @Test
-       public void fcpInterfaceCanBeActivated() {
-               fcpInterface.fcpInterfaceActivated(new FcpInterfaceActivatedEvent());
-               assertThat(fcpInterface.isActive(), is(true));
-       }
-
-       @Test
-       public void fcpInterfaceCanBeDeactivated() {
-               fcpInterface.fcpInterfaceDeactivated(new FcpInterfaceDeactivatedEvent());
-               assertThat(fcpInterface.isActive(), is(false));
-       }
-
-       @Test
-       public void setFullAccessRequiredCanSetAccessToNo() {
-               fcpInterface.fullAccessRequiredChanged(
-                               new FullAccessRequiredChanged(NO));
-               assertThat(fcpInterface.getFullAccessRequired(), is(NO));
-       }
-
-       @Test
-       public void setFullAccessRequiredCanSetAccessToWriting() {
-               fcpInterface.fullAccessRequiredChanged(
-                               new FullAccessRequiredChanged(WRITING));
-               assertThat(fcpInterface.getFullAccessRequired(), is(WRITING));
-       }
-
-       @Test
-       public void setFullAccessRequiredCanSetAccessToAlways() {
-               fcpInterface.fullAccessRequiredChanged(
-                               new FullAccessRequiredChanged(ALWAYS));
-               assertThat(fcpInterface.getFullAccessRequired(), is(ALWAYS));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java b/src/test/java/net/pterodactylus/sone/fcp/LockSoneCommandTest.java
deleted file mode 100644 (file)
index 8d6ee61..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Sone - LockSoneCommandTest.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.fcp;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
-import net.pterodactylus.sone.freenet.fcp.Command.Response;
-import net.pterodactylus.sone.freenet.fcp.FcpException;
-
-import freenet.support.SimpleFieldSet;
-
-import com.google.common.base.Optional;
-import org.junit.Test;
-
-/**
- * Tests for {@link UnlockSoneCommand}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LockSoneCommandTest {
-
-       @Test
-       public void testLockingALocalSone() throws FcpException {
-               Sone localSone = mock(Sone.class);
-               when(localSone.getId()).thenReturn("LocalSone");
-               when(localSone.isLocal()).thenReturn(true);
-               Core core = mock(Core.class);
-               when(core.getSone(eq("LocalSone"))).thenReturn(Optional.of(localSone));
-               when(core.getLocalSone(eq("LocalSone"))).thenReturn(localSone);
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "LocalSone").get();
-
-               LockSoneCommand lockSoneCommand = new LockSoneCommand(core);
-               Response response = lockSoneCommand.execute(fields, null, null);
-
-               verify(core).lockSone(eq(localSone));
-               assertThat(response, notNullValue());
-               assertThat(response.getReplyParameters(), notNullValue());
-               assertThat(response.getReplyParameters().get("Message"), is("SoneLocked"));
-               assertThat(response.getReplyParameters().get("Sone"), is("LocalSone"));
-       }
-
-       @Test(expected = FcpException.class)
-       public void testLockingARemoteSone() throws FcpException {
-               Sone removeSone = mock(Sone.class);
-               Core core = mock(Core.class);
-               when(core.getSone(eq("RemoteSone"))).thenReturn(Optional.of(removeSone));
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "RemoteSone").get();
-
-               LockSoneCommand lockSoneCommand = new LockSoneCommand(core);
-               lockSoneCommand.execute(fields, null, null);
-       }
-
-       @Test(expected = FcpException.class)
-       public void testMissingSone() throws FcpException {
-               Core core = mock(Core.class);
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().get();
-
-               LockSoneCommand lockSoneCommand = new LockSoneCommand(core);
-               lockSoneCommand.execute(fields, null, null);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java b/src/test/java/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.java
deleted file mode 100644 (file)
index 842d0e8..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Sone - UnlockSoneCommandTest.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.fcp;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
-import net.pterodactylus.sone.freenet.fcp.Command.Response;
-import net.pterodactylus.sone.freenet.fcp.FcpException;
-
-import freenet.support.SimpleFieldSet;
-
-import com.google.common.base.Optional;
-import org.junit.Test;
-
-/**
- * Tests for {@link LockSoneCommand}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UnlockSoneCommandTest {
-
-       @Test
-       public void testUnlockingALocalSone() throws FcpException {
-               Sone localSone = mock(Sone.class);
-               when(localSone.getId()).thenReturn("LocalSone");
-               when(localSone.isLocal()).thenReturn(true);
-               Core core = mock(Core.class);
-               when(core.getSone(eq("LocalSone"))).thenReturn(Optional.of(localSone));
-               when(core.getLocalSone(eq("LocalSone"))).thenReturn(localSone);
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "LocalSone").get();
-
-               UnlockSoneCommand unlockSoneCommand = new UnlockSoneCommand(core);
-               Response response = unlockSoneCommand.execute(fields, null, null);
-
-               verify(core).unlockSone(eq(localSone));
-               assertThat(response, notNullValue());
-               assertThat(response.getReplyParameters(), notNullValue());
-               assertThat(response.getReplyParameters().get("Message"), is("SoneUnlocked"));
-               assertThat(response.getReplyParameters().get("Sone"), is("LocalSone"));
-       }
-
-       @Test(expected = FcpException.class)
-       public void testUnlockingARemoteSone() throws FcpException {
-               Sone removeSone = mock(Sone.class);
-               Core core = mock(Core.class);
-               when(core.getSone(eq("RemoteSone"))).thenReturn(Optional.of(removeSone));
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().put("Sone", "RemoteSone").get();
-
-               UnlockSoneCommand unlockSoneCommand = new UnlockSoneCommand(core);
-               unlockSoneCommand.execute(fields, null, null);
-       }
-
-       @Test(expected = FcpException.class)
-       public void testMissingSone() throws FcpException {
-               Core core = mock(Core.class);
-               SimpleFieldSet fields = new SimpleFieldSetBuilder().get();
-
-               UnlockSoneCommand unlockSoneCommand = new UnlockSoneCommand(core);
-               unlockSoneCommand.execute(fields, null, null);
-       }
-
-}
index f11c7e3..8def0e0 100644 (file)
@@ -19,7 +19,7 @@ package net.pterodactylus.sone.freenet.wot;
 
 import static com.google.common.collect.ImmutableMap.of;
 import static java.util.Arrays.asList;
-import static net.pterodactylus.sone.Matchers.matchesRegex;
+import static net.pterodactylus.sone.test.Matchers.matchesRegex;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
index 7ae3dd8..ea5f067 100644 (file)
@@ -110,6 +110,7 @@ public class ListNotificationTest {
                ListNotification secondNotification = new ListNotification(listNotification);
                listNotification.add("a");
                secondNotification.add("a");
+               listNotification.setLastUpdateTime(secondNotification.getLastUpdatedTime());
                assertThat(listNotification.hashCode(), is(secondNotification.hashCode()));
        }
 
index aace929..56d2f29 100644 (file)
@@ -14,11 +14,11 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import net.pterodactylus.sone.TestUtil;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.test.TestUtil;
 
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
index cd989eb..7b93477 100644 (file)
@@ -2,6 +2,7 @@ package net.pterodactylus.sone.template;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
 
 import java.io.File;
 import java.io.IOException;
@@ -108,4 +109,19 @@ public class FilesystemTemplateTest {
                assertThat(getRenderedString(), is("New.a1.Test"));
        }
 
+       @Test
+       public void columnOfReturnedTemplateIsReturnedAsZero() {
+               assertThat(filesystemTemplate.getColumn(), is(0));
+       }
+
+       @Test
+       public void lineOfReturnedTemplateIsReturnedAsZero() {
+               assertThat(filesystemTemplate.getLine(), is(0));
+       }
+
+       @Test
+       public void templateCanBeIteratedOver() {
+           assertThat(filesystemTemplate.iterator(), notNullValue());
+       }
+
 }
index 33149e0..6744bd8 100644 (file)
@@ -2,6 +2,8 @@ package net.pterodactylus.sone.template;
 
 import static java.util.Arrays.asList;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.sameInstance;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -12,7 +14,10 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.test.GuiceKt;
 
+import com.google.inject.Guice;
+import com.google.inject.Injector;
 import org.hamcrest.Matchers;
 import org.junit.Before;
 import org.junit.Test;
@@ -78,4 +83,22 @@ public class IdentityAccessorTest {
                                Matchers.<Object>is(identity.hashCode()));
        }
 
+       @Test
+       public void accessorCanBeCreatedByGuice() {
+               Injector injector = Guice.createInjector(
+                               GuiceKt.supply(Core.class).byInstance(mock(Core.class))
+               );
+               assertThat(injector.getInstance(IdentityAccessor.class), notNullValue());
+       }
+
+       @Test
+       public void accessorIsCreatedAsSingleton() {
+               Injector injector = Guice.createInjector(
+                               GuiceKt.supply(Core.class).byInstance(mock(Core.class))
+               );
+               IdentityAccessor firstAccessor = injector.getInstance(IdentityAccessor.class);
+               IdentityAccessor secondAccessor = injector.getInstance(IdentityAccessor.class);
+               assertThat(firstAccessor, sameInstance(secondAccessor));
+       }
+
 }
diff --git a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java
new file mode 100644 (file)
index 0000000..a6bc381
--- /dev/null
@@ -0,0 +1,120 @@
+package net.pterodactylus.sone.template;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.TemplateContext;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link PostAccessor}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PostAccessorTest {
+
+       private final Core core = mock(Core.class);
+       private final PostAccessor accessor = new PostAccessor(core);
+       private final Post post = mock(Post.class);
+
+       private final long now = System.currentTimeMillis();
+
+       @Before
+       public void setupPost() {
+               when(post.getId()).thenReturn("post-id");
+       }
+
+       @Test
+       @SuppressWarnings("unchecked")
+       public void accessorReturnsTheCorrectReplies() {
+               List<PostReply> replies = new ArrayList<>();
+               replies.add(createPostReply(2000));
+               replies.add(createPostReply(-1000));
+               replies.add(createPostReply(-2000));
+               replies.add(createPostReply(-3000));
+               replies.add(createPostReply(-4000));
+               when(core.getReplies("post-id")).thenReturn(replies);
+               Collection<PostReply> repliesForPost = (Collection<PostReply>) accessor.get(null, post, "replies");
+               assertThat(repliesForPost, contains(
+                               replies.get(1),
+                               replies.get(2),
+                               replies.get(3),
+                               replies.get(4)
+               ));
+       }
+
+       private PostReply createPostReply(long timeOffset) {
+               PostReply postReply = mock(PostReply.class);
+               when(postReply.getTime()).thenReturn(now + timeOffset);
+               return postReply;
+       }
+
+       @Test
+       @SuppressWarnings("unchecked")
+       public void accessorReturnsTheLikingSones() {
+               Set<Sone> sones = mock(Set.class);
+               when(core.getLikes(post)).thenReturn(sones);
+               Set<Sone> likingSones = (Set<Sone>) accessor.get(null, post, "likes");
+               assertThat(likingSones, is(sones));
+       }
+
+       @Test
+       public void accessorReturnsWhetherTheCurrentSoneLikedAPost() {
+               Sone sone = mock(Sone.class);
+               when(sone.isLikedPostId("post-id")).thenReturn(true);
+               TemplateContext templateContext = new TemplateContext();
+               templateContext.set("currentSone", sone);
+               assertThat(accessor.get(templateContext, post, "liked"), is((Object) true));
+       }
+
+       @Test
+       public void accessorReturnsFalseIfPostIsNotLiked() {
+               Sone sone = mock(Sone.class);
+               TemplateContext templateContext = new TemplateContext();
+               templateContext.set("currentSone", sone);
+               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
+       }
+
+       @Test
+       public void accessorReturnsFalseIfThereIsNoCurrentSone() {
+               TemplateContext templateContext = new TemplateContext();
+               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
+       }
+
+       @Test
+       public void accessorReturnsThatNotKnownPostIsNew() {
+               assertThat(accessor.get(null, post, "new"), is((Object) true));
+       }
+
+       @Test
+       public void accessorReturnsThatKnownPostIsNotNew() {
+               when(post.isKnown()).thenReturn(true);
+               assertThat(accessor.get(null, post, "new"), is((Object) false));
+       }
+
+       @Test
+       public void accessorReturnsIfPostIsBookmarked() {
+               when(core.isBookmarked(post)).thenReturn(true);
+               assertThat(accessor.get(null, post, "bookmarked"), is((Object) true));
+       }
+
+       @Test
+       public void accessorReturnsOtherProperties() {
+               assertThat(accessor.get(null, post, "hashCode"), is((Object) post.hashCode()));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/Matchers.java b/src/test/java/net/pterodactylus/sone/test/Matchers.java
new file mode 100644 (file)
index 0000000..08ad611
--- /dev/null
@@ -0,0 +1,373 @@
+/*
+ * Sone - Matchers.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.test;
+
+import static java.util.regex.Pattern.compile;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+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 com.google.common.base.Optional;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Matchers used throughout the tests.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Matchers {
+
+       public static Matcher<String> matchesRegex(final String regex) {
+               return new TypeSafeMatcher<String>() {
+                       @Override
+                       protected boolean matchesSafely(String item) {
+                               return compile(regex).matcher(item).matches();
+                       }
+
+                       @Override
+                       public void describeTo(Description description) {
+                               description.appendText("matches: ").appendValue(regex);
+                       }
+               };
+       }
+
+       public static Matcher<InputStream> delivers(final byte[] data) {
+               return new TypeSafeMatcher<InputStream>() {
+                       byte[] readData = new byte[data.length];
+
+                       @Override
+                       protected boolean matchesSafely(InputStream inputStream) {
+                               int offset = 0;
+                               try {
+                                       while (true) {
+                                               int r = inputStream.read();
+                                               if (r == -1) {
+                                                       return offset == data.length;
+                                               }
+                                               if (offset == data.length) {
+                                                       return false;
+                                               }
+                                               if (data[offset] != (readData[offset] = (byte) r)) {
+                                                       return false;
+                                               }
+                                               offset++;
+                                       }
+                               } catch (IOException ioe1) {
+                                       return false;
+                               }
+                       }
+
+                       @Override
+                       public void describeTo(Description description) {
+                               description.appendValue(data);
+                       }
+
+                       @Override
+                       protected void describeMismatchSafely(InputStream item,
+                                       Description mismatchDescription) {
+                               mismatchDescription.appendValue(readData);
+                       }
+               };
+       }
+
+       public static Matcher<Post> isPost(String postId, long time,
+                       String text, Optional<String> recipient) {
+               return new PostMatcher(postId, time, text, recipient);
+       }
+
+       public static Matcher<Post> isPostWithId(String postId) {
+               return new PostIdMatcher(postId);
+       }
+
+       public static Matcher<PostReply> isPostReply(String postReplyId,
+                       String postId, long time, String text) {
+               return new PostReplyMatcher(postReplyId, postId, time, text);
+       }
+
+       public static Matcher<Album> isAlbum(final String albumId,
+                       final String parentAlbumId,
+                       final String title, final String albumDescription) {
+               return new TypeSafeDiagnosingMatcher<Album>() {
+                       @Override
+                       protected boolean matchesSafely(Album album,
+                                       Description mismatchDescription) {
+                               if (!album.getId().equals(albumId)) {
+                                       mismatchDescription.appendText("ID is ")
+                                                       .appendValue(album.getId());
+                                       return false;
+                               }
+                               if (parentAlbumId == null) {
+                                       if (album.getParent() != null) {
+                                               mismatchDescription.appendText("has parent album");
+                                               return false;
+                                       }
+                               } else {
+                                       if (album.getParent() == null) {
+                                               mismatchDescription.appendText("has no parent album");
+                                               return false;
+                                       }
+                                       if (!album.getParent().getId().equals(parentAlbumId)) {
+                                               mismatchDescription.appendText("parent album is ")
+                                                               .appendValue(album.getParent().getId());
+                                               return false;
+                                       }
+                               }
+                               if (!title.equals(album.getTitle())) {
+                                       mismatchDescription.appendText("has title ")
+                                                       .appendValue(album.getTitle());
+                                       return false;
+                               }
+                               if (!albumDescription.equals(album.getDescription())) {
+                                       mismatchDescription.appendText("has description ")
+                                                       .appendValue(album.getDescription());
+                                       return false;
+                               }
+                               return true;
+                       }
+
+                       @Override
+                       public void describeTo(Description description) {
+                               description.appendText("is album ").appendValue(albumId);
+                               if (parentAlbumId == null) {
+                                       description.appendText(", has no parent");
+                               } else {
+                                       description.appendText(", has parent ")
+                                                       .appendValue(parentAlbumId);
+                               }
+                               description.appendText(", has title ").appendValue(title);
+                               description.appendText(", has description ")
+                                               .appendValue(albumDescription);
+                       }
+               };
+       }
+
+       public static Matcher<Image> isImage(final String id,
+                       final long creationTime,
+                       final String key, final String title,
+                       final String imageDescription,
+                       final int width, final int height) {
+               return new TypeSafeDiagnosingMatcher<Image>() {
+                       @Override
+                       protected boolean matchesSafely(Image image,
+                                       Description mismatchDescription) {
+                               if (!image.getId().equals(id)) {
+                                       mismatchDescription.appendText("ID is ")
+                                                       .appendValue(image.getId());
+                                       return false;
+                               }
+                               if (image.getCreationTime() != creationTime) {
+                                       mismatchDescription.appendText("created at @")
+                                                       .appendValue(image.getCreationTime());
+                                       return false;
+                               }
+                               if (!image.getKey().equals(key)) {
+                                       mismatchDescription.appendText("key is ")
+                                                       .appendValue(image.getKey());
+                                       return false;
+                               }
+                               if (!image.getTitle().equals(title)) {
+                                       mismatchDescription.appendText("title is ")
+                                                       .appendValue(image.getTitle());
+                                       return false;
+                               }
+                               if (!image.getDescription().equals(imageDescription)) {
+                                       mismatchDescription.appendText("description is ")
+                                                       .appendValue(image.getDescription());
+                                       return false;
+                               }
+                               if (image.getWidth() != width) {
+                                       mismatchDescription.appendText("width is ")
+                                                       .appendValue(image.getWidth());
+                                       return false;
+                               }
+                               if (image.getHeight() != height) {
+                                       mismatchDescription.appendText("height is ")
+                                                       .appendValue(image.getHeight());
+                                       return false;
+                               }
+                               return true;
+                       }
+
+                       @Override
+                       public void describeTo(Description description) {
+                               description.appendText("image with ID ").appendValue(id);
+                               description.appendText(", created at @")
+                                               .appendValue(creationTime);
+                               description.appendText(", has key ").appendValue(key);
+                               description.appendText(", has title ").appendValue(title);
+                               description.appendText(", has description ")
+                                               .appendValue(imageDescription);
+                               description.appendText(", has width ").appendValue(width);
+                               description.appendText(", has height ").appendValue(height);
+                       }
+               };
+       }
+
+       private static class PostMatcher extends TypeSafeDiagnosingMatcher<Post> {
+
+               private final String postId;
+               private final long time;
+               private final String text;
+               private final Optional<String> recipient;
+
+               private PostMatcher(String postId, long time, String text,
+                               Optional<String> recipient) {
+                       this.postId = postId;
+                       this.time = time;
+                       this.text = text;
+                       this.recipient = recipient;
+               }
+
+               @Override
+               protected boolean matchesSafely(Post post,
+                               Description mismatchDescription) {
+                       if (!post.getId().equals(postId)) {
+                               mismatchDescription.appendText("ID is not ")
+                                               .appendValue(postId);
+                               return false;
+                       }
+                       if (post.getTime() != time) {
+                               mismatchDescription.appendText("Time is not @")
+                                               .appendValue(time);
+                               return false;
+                       }
+                       if (!post.getText().equals(text)) {
+                               mismatchDescription.appendText("Text is not ")
+                                               .appendValue(text);
+                               return false;
+                       }
+                       if (recipient.isPresent()) {
+                               if (!post.getRecipientId().isPresent()) {
+                                       mismatchDescription.appendText(
+                                                       "Recipient not present");
+                                       return false;
+                               }
+                               if (!post.getRecipientId().get().equals(recipient.get())) {
+                                       mismatchDescription.appendText("Recipient is not ")
+                                                       .appendValue(recipient.get());
+                                       return false;
+                               }
+                       } else {
+                               if (post.getRecipientId().isPresent()) {
+                                       mismatchDescription.appendText("Recipient is present");
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+
+               @Override
+               public void describeTo(Description description) {
+                       description.appendText("is post with ID ")
+                                       .appendValue(postId);
+                       description.appendText(", created at @").appendValue(time);
+                       description.appendText(", text ").appendValue(text);
+                       if (recipient.isPresent()) {
+                               description.appendText(", directed at ")
+                                               .appendValue(recipient.get());
+                       }
+               }
+
+       }
+
+       private static class PostIdMatcher extends TypeSafeDiagnosingMatcher<Post> {
+
+               private final String id;
+
+               private PostIdMatcher(String id) {
+                       this.id = id;
+               }
+
+               @Override
+               protected boolean matchesSafely(Post item,
+                               Description mismatchDescription) {
+                       if (!item.getId().equals(id)) {
+                               mismatchDescription.appendText("post has ID ").appendValue(item.getId());
+                               return false;
+                       }
+                       return true;
+               }
+
+               @Override
+               public void describeTo(Description description) {
+                       description.appendText("post with ID ").appendValue(id);
+               }
+
+       }
+
+       private static class PostReplyMatcher
+                       extends TypeSafeDiagnosingMatcher<PostReply> {
+
+               private final String postReplyId;
+               private final String postId;
+               private final long time;
+               private final String text;
+
+               private PostReplyMatcher(String postReplyId, String postId, long time,
+                               String text) {
+                       this.postReplyId = postReplyId;
+                       this.postId = postId;
+                       this.time = time;
+                       this.text = text;
+               }
+
+               @Override
+               protected boolean matchesSafely(PostReply postReply,
+                               Description mismatchDescription) {
+                       if (!postReply.getId().equals(postReplyId)) {
+                               mismatchDescription.appendText("is post reply ")
+                                               .appendValue(postReply.getId());
+                               return false;
+                       }
+                       if (!postReply.getPostId().equals(postId)) {
+                               mismatchDescription.appendText("is reply to ")
+                                               .appendValue(postReply.getPostId());
+                               return false;
+                       }
+                       if (postReply.getTime() != time) {
+                               mismatchDescription.appendText("is created at @").appendValue(
+                                               postReply.getTime());
+                               return false;
+                       }
+                       if (!postReply.getText().equals(text)) {
+                               mismatchDescription.appendText("says ")
+                                               .appendValue(postReply.getText());
+                               return false;
+                       }
+                       return true;
+               }
+
+               @Override
+               public void describeTo(Description description) {
+                       description.appendText("is post reply ").appendValue(postReplyId);
+                       description.appendText(", replies to post ").appendValue(postId);
+                       description.appendText(", is created at @").appendValue(time);
+                       description.appendText(", says ").appendValue(text);
+               }
+
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestAlbumBuilder.java b/src/test/java/net/pterodactylus/sone/test/TestAlbumBuilder.java
new file mode 100644 (file)
index 0000000..8871ee3
--- /dev/null
@@ -0,0 +1,122 @@
+package net.pterodactylus.sone.test;
+
+import static java.util.UUID.randomUUID;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Album.Modifier;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.AlbumBuilder;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * {@link AlbumBuilder} that returns a mocked {@link Album}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestAlbumBuilder implements AlbumBuilder {
+
+       private final Album album = mock(Album.class);
+       private final List<Album> albums = new ArrayList<Album>();
+       private final List<Image> images = new ArrayList<Image>();
+       private Album parentAlbum;
+       private String title;
+       private String description;
+       private String imageId;
+
+       public TestAlbumBuilder() {
+               when(album.getTitle()).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) {
+                               return title;
+                       }
+               });
+               when(album.getDescription()).thenAnswer(new Answer<String>() {
+                       @Override
+                       public String answer(InvocationOnMock invocation) {
+                               return description;
+                       }
+               });
+               when(album.getAlbums()).thenReturn(albums);
+               when(album.getImages()).thenReturn(images);
+               doAnswer(new Answer<Void>() {
+                       @Override
+                       public Void answer(InvocationOnMock invocation) {
+                               albums.add((Album) invocation.getArguments()[0]);
+                               ((Album) invocation.getArguments()[0]).setParent(album);
+                               return null;
+                       }
+               }).when(album).addAlbum(any(Album.class));
+               doAnswer(new Answer<Void>() {
+                       @Override
+                       public Void answer(InvocationOnMock invocation) {
+                               images.add((Image) invocation.getArguments()[0]);
+                               return null;
+                       }
+               }).when(album).addImage(any(Image.class));
+               doAnswer(new Answer<Void>() {
+                       @Override
+                       public Void answer(InvocationOnMock invocation) {
+                               parentAlbum = (Album) invocation.getArguments()[0];
+                               return null;
+                       }
+               }).when(album).setParent(any(Album.class));
+               when(album.getParent()).thenAnswer(new Answer<Album>() {
+                       @Override
+                       public Album answer(InvocationOnMock invocation) {
+                               return parentAlbum;
+                       }
+               });
+               when(album.modify()).thenReturn(new Modifier() {
+                       @Override
+                       public Modifier setTitle(String title) {
+                               TestAlbumBuilder.this.title = title;
+                               return this;
+                       }
+
+                       @Override
+                       public Modifier setDescription(String description) {
+                               TestAlbumBuilder.this.description = description;
+                               return this;
+                       }
+
+                       @Override
+                       public Album update() throws IllegalStateException {
+                               return album;
+                       }
+               });
+       }
+
+       @Override
+       public AlbumBuilder randomId() {
+               when(album.getId()).thenReturn(randomUUID().toString());
+               return this;
+       }
+
+       @Override
+       public AlbumBuilder withId(String id) {
+               when(album.getId()).thenReturn(id);
+               return this;
+       }
+
+       @Override
+       public AlbumBuilder by(Sone sone) {
+               when(album.getSone()).thenReturn(sone);
+               return this;
+       }
+
+       @Override
+       public Album build() throws IllegalStateException {
+               return album;
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestImageBuilder.java b/src/test/java/net/pterodactylus/sone/test/TestImageBuilder.java
new file mode 100644 (file)
index 0000000..edeeb1a
--- /dev/null
@@ -0,0 +1,105 @@
+package net.pterodactylus.sone.test;
+
+import static java.util.UUID.randomUUID;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.ImageBuilder;
+
+/**
+ * {@link ImageBuilder} implementation that returns a mocked {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestImageBuilder implements ImageBuilder {
+
+       private final Image image;
+
+       public TestImageBuilder() {
+               image = mock(Image.class);
+               Image.Modifier imageModifier = new Image.Modifier() {
+                       private Sone sone = image.getSone();
+                       private long creationTime = image.getCreationTime();
+                       private String key = image.getKey();
+                       private String title = image.getTitle();
+                       private String description = image.getDescription();
+                       private int width = image.getWidth();
+                       private int height = image.getHeight();
+
+                       @Override
+                       public Image.Modifier setSone(Sone sone) {
+                               this.sone = sone;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setCreationTime(long creationTime) {
+                               this.creationTime = creationTime;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setKey(String key) {
+                               this.key = key;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setTitle(String title) {
+                               this.title = title;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setDescription(String description) {
+                               this.description = description;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setWidth(int width) {
+                               this.width = width;
+                               return this;
+                       }
+
+                       @Override
+                       public Image.Modifier setHeight(int height) {
+                               this.height = height;
+                               return this;
+                       }
+
+                       @Override
+                       public Image update() throws IllegalStateException {
+                               when(image.getSone()).thenReturn(sone);
+                               when(image.getCreationTime()).thenReturn(creationTime);
+                               when(image.getKey()).thenReturn(key);
+                               when(image.getTitle()).thenReturn(title);
+                               when(image.getDescription()).thenReturn(description);
+                               when(image.getWidth()).thenReturn(width);
+                               when(image.getHeight()).thenReturn(height);
+                               return image;
+                       }
+               };
+               when(image.modify()).thenReturn(imageModifier);
+       }
+
+       @Override
+       public ImageBuilder randomId() {
+               when(image.getId()).thenReturn(randomUUID().toString());
+               return this;
+       }
+
+       @Override
+       public ImageBuilder withId(String id) {
+               when(image.getId()).thenReturn(id);
+               return this;
+       }
+
+       @Override
+       public Image build() throws IllegalStateException {
+               return image;
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestPostBuilder.java b/src/test/java/net/pterodactylus/sone/test/TestPostBuilder.java
new file mode 100644 (file)
index 0000000..1933857
--- /dev/null
@@ -0,0 +1,78 @@
+package net.pterodactylus.sone.test;
+
+import static com.google.common.base.Optional.fromNullable;
+import static java.lang.System.currentTimeMillis;
+import static java.util.UUID.randomUUID;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.PostBuilder;
+
+/**
+ * {@link PostBuilder} implementation that returns a mocked {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestPostBuilder implements PostBuilder {
+
+       private final Post post = mock(Post.class);
+       private String recipientId = null;
+
+       @Override
+       public PostBuilder copyPost(Post post) throws NullPointerException {
+               return this;
+       }
+
+       @Override
+       public PostBuilder from(String senderId) {
+               final Sone sone = mock(Sone.class);
+               when(sone.getId()).thenReturn(senderId);
+               when(post.getSone()).thenReturn(sone);
+               return this;
+       }
+
+       @Override
+       public PostBuilder randomId() {
+               when(post.getId()).thenReturn(randomUUID().toString());
+               return this;
+       }
+
+       @Override
+       public PostBuilder withId(String id) {
+               when(post.getId()).thenReturn(id);
+               return this;
+       }
+
+       @Override
+       public PostBuilder currentTime() {
+               when(post.getTime()).thenReturn(currentTimeMillis());
+               return this;
+       }
+
+       @Override
+       public PostBuilder withTime(long time) {
+               when(post.getTime()).thenReturn(time);
+               return this;
+       }
+
+       @Override
+       public PostBuilder withText(String text) {
+               when(post.getText()).thenReturn(text);
+               return this;
+       }
+
+       @Override
+       public PostBuilder to(String recipientId) {
+               this.recipientId = recipientId;
+               return this;
+       }
+
+       @Override
+       public Post build() throws IllegalStateException {
+               when(post.getRecipientId()).thenReturn(fromNullable(recipientId));
+               return post;
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestPostReplyBuilder.java b/src/test/java/net/pterodactylus/sone/test/TestPostReplyBuilder.java
new file mode 100644 (file)
index 0000000..e09e120
--- /dev/null
@@ -0,0 +1,70 @@
+package net.pterodactylus.sone.test;
+
+import static java.lang.System.currentTimeMillis;
+import static java.util.UUID.randomUUID;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.PostReplyBuilder;
+
+/**
+ * {@link PostReplyBuilder} that returns a mocked {@link PostReply}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestPostReplyBuilder implements PostReplyBuilder {
+
+       private final PostReply postReply = mock(PostReply.class);
+
+       @Override
+       public PostReplyBuilder to(String postId) {
+               when(postReply.getPostId()).thenReturn(postId);
+               return this;
+       }
+
+       @Override
+       public PostReply build() throws IllegalStateException {
+               return postReply;
+       }
+
+       @Override
+       public PostReplyBuilder randomId() {
+               when(postReply.getId()).thenReturn(randomUUID().toString());
+               return this;
+       }
+
+       @Override
+       public PostReplyBuilder withId(String id) {
+               when(postReply.getId()).thenReturn(id);
+               return this;
+       }
+
+       @Override
+       public PostReplyBuilder from(String senderId) {
+               Sone sone = mock(Sone.class);
+               when(sone.getId()).thenReturn(senderId);
+               when(postReply.getSone()).thenReturn(sone);
+               return this;
+       }
+
+       @Override
+       public PostReplyBuilder currentTime() {
+               when(postReply.getTime()).thenReturn(currentTimeMillis());
+               return this;
+       }
+
+       @Override
+       public PostReplyBuilder withTime(long time) {
+               when(postReply.getTime()).thenReturn(time);
+               return this;
+       }
+
+       @Override
+       public PostReplyBuilder withText(String text) {
+               when(postReply.getText()).thenReturn(text);
+               return this;
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestUtil.java b/src/test/java/net/pterodactylus/sone/test/TestUtil.java
new file mode 100644 (file)
index 0000000..8b3160f
--- /dev/null
@@ -0,0 +1,56 @@
+package net.pterodactylus.sone.test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Utilities for testing.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestUtil {
+
+       public static void setFinalField(Object object, String fieldName, Object value) {
+               try {
+                       Field clientCoreField = object.getClass().getField(fieldName);
+                       clientCoreField.setAccessible(true);
+                       Field modifiersField = Field.class.getDeclaredField("modifiers");
+                       modifiersField.setAccessible(true);
+                       modifiersField.setInt(clientCoreField, clientCoreField.getModifiers() & ~Modifier.FINAL);
+                       clientCoreField.set(object, value);
+               } catch (NoSuchFieldException e) {
+                       throw new RuntimeException(e);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       public static <T> T getPrivateField(Object object, String fieldName) {
+               try {
+                       Field field = object.getClass().getDeclaredField(fieldName);
+                       field.setAccessible(true);
+                       return (T) field.get(object);
+               } catch (NoSuchFieldException e) {
+                       throw new RuntimeException(e);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       public static <T> T callPrivateMethod(Object object, String methodName) {
+               try {
+                       Method method = object.getClass().getDeclaredMethod(methodName, new Class[0]);
+                       method.setAccessible(true);
+                       return (T) method.invoke(object);
+               } catch (NoSuchMethodException e) {
+                       throw new RuntimeException(e);
+               } catch (InvocationTargetException e) {
+                       throw new RuntimeException(e);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/test/TestValue.java b/src/test/java/net/pterodactylus/sone/test/TestValue.java
new file mode 100644 (file)
index 0000000..94ff3c6
--- /dev/null
@@ -0,0 +1,59 @@
+package net.pterodactylus.sone.test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.pterodactylus.util.config.ConfigurationException;
+import net.pterodactylus.util.config.Value;
+
+import com.google.common.base.Objects;
+
+/**
+ * Simple {@link Value} implementation.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TestValue<T> implements Value<T> {
+
+       private final AtomicReference<T> value = new AtomicReference<T>();
+
+       public TestValue(T originalValue) {
+               value.set(originalValue);
+       }
+
+       @Override
+       public T getValue() throws ConfigurationException {
+               return value.get();
+       }
+
+       @Override
+       public T getValue(T defaultValue) {
+               final T realValue = value.get();
+               return (realValue != null) ? realValue : defaultValue;
+       }
+
+       @Override
+       public void setValue(T newValue) throws ConfigurationException {
+               value.set(newValue);
+       }
+
+       @Override
+       public int hashCode() {
+               return value.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               return (obj instanceof TestValue) && Objects.equal(value.get(),
+                               ((TestValue) obj).value.get());
+       }
+
+       @Override
+       public String toString() {
+               return String.valueOf(value.get());
+       }
+
+       public static <T> Value<T> from(T value) {
+               return new TestValue<T>(value);
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/text/FreemailPartTest.java b/src/test/java/net/pterodactylus/sone/text/FreemailPartTest.java
new file mode 100644 (file)
index 0000000..49972df
--- /dev/null
@@ -0,0 +1,37 @@
+package net.pterodactylus.sone.text;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link FreemailPart}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreemailPartTest {
+
+       private final FreemailPart part = new FreemailPart("local", "freemail-id", "identity-id");
+
+       @Test
+       public void freemailPartRetainsEmailLocalPart() {
+               assertThat(part.getEmailLocalPart(), is("local"));
+       }
+
+       @Test
+       public void freemailPartRetainsFreemailId() {
+               assertThat(part.getFreemailId(), is("freemail-id"));
+       }
+
+       @Test
+       public void freemailPartRetainsIdentityId() {
+               assertThat(part.getIdentityId(), is("identity-id"));
+       }
+
+       @Test
+       public void freemailPartReturnsCorrectText() {
+               assertThat(part.getText(), is("local@freemail-id.freemail"));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/text/FreenetLinkPartTest.java b/src/test/java/net/pterodactylus/sone/text/FreenetLinkPartTest.java
deleted file mode 100644 (file)
index f29fdde..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.pterodactylus.sone.text;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link FreenetLinkPart}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkPartTest {
-
-       private final FreenetLinkPart part = new FreenetLinkPart("link", "text", "title", true);
-
-       @Test
-       public void linkIsRetainedCorrectly() {
-               assertThat(part.getLink(), is("link"));
-       }
-
-       @Test
-       public void textIsRetainedCorrectly() {
-               assertThat(part.getText(), is("text"));
-       }
-
-       @Test
-       public void titleIsRetainedCorrectly() {
-               assertThat(part.getTitle(), is("title"));
-       }
-
-       @Test
-       public void trustedIsRetainedCorrectly() {
-               assertThat(part.isTrusted(), is(true));
-       }
-
-       @Test
-       public void textIsUsedAsTitleIfNoTextIsGiven() {
-               assertThat(new FreenetLinkPart("link", "text", true).getTitle(), is("text"));
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForLink() {
-               new FreenetLinkPart(null, "text", "title", true);
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForText() {
-               new FreenetLinkPart("link", null, "title", true);
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForLinkInSecondaryConstructor() {
-               new FreenetLinkPart(null, "text", true);
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForTextInSecondaryConstructor() {
-               new FreenetLinkPart("link", null, true);
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForTitle() {
-               new FreenetLinkPart("link", "text", null, true);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/text/LinkPartTest.java b/src/test/java/net/pterodactylus/sone/text/LinkPartTest.java
deleted file mode 100644 (file)
index 431dfa6..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.pterodactylus.sone.text;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link LinkPart}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class LinkPartTest {
-
-       private final LinkPart part = new LinkPart("link", "text", "title");
-
-       @Test
-       public void linkIsRetainedCorrectly() {
-               assertThat(part.getLink(), is("link"));
-       }
-
-       @Test
-       public void textIsRetainedCorrectly() {
-               assertThat(part.getText(), is("text"));
-       }
-
-       @Test
-       public void titleIsRetainedCorrectly() {
-               assertThat(part.getTitle(), is("title"));
-       }
-
-       @Test
-       public void textIsUsedAsTitleIfNoTitleIsGiven() {
-               assertThat(new LinkPart("link", "text").getTitle(), is("text"));
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForLink() {
-               new LinkPart(null, "text", "title");
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForText() {
-               new LinkPart("link", null, "title");
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForLinkInSecondaryConstructor() {
-               new LinkPart(null, "text");
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForTextInSecondaryConstructor() {
-               new LinkPart("link", null);
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForTitle() {
-               new LinkPart("link", "text", null);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/text/PartContainerTest.java b/src/test/java/net/pterodactylus/sone/text/PartContainerTest.java
deleted file mode 100644 (file)
index 036b2dc..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-package net.pterodactylus.sone.text;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link PartContainer}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PartContainerTest {
-
-       private final PartContainer container = new PartContainer();
-
-       @Test
-       public void emptyContainerHasSizeZero() {
-               assertThat(container.size(), is(0));
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void canNotAddNullPart() {
-           container.add(null);
-       }
-
-       @Test
-       public void containerWithSinglePartHasSizeOne() {
-               container.add(mock(Part.class));
-               assertThat(container.size(), is(1));
-       }
-
-       @Test
-       public void containerWithSinglePartCanReturnPart() {
-               Part part = mock(Part.class);
-               container.add(part);
-               assertThat(container.getPart(0), is(part));
-       }
-
-       @Test
-       public void containerIsEmptyAfterPartIsAddedAndRemoved() {
-               container.add(mock(Part.class));
-               container.removePart(0);
-               assertThat(container.size(), is(0));
-       }
-
-       @Test
-       public void containerContainsSecondPartIfFirstPartIsRemoved() {
-               container.add(mock(Part.class));
-               Part part = mock(Part.class);
-               container.add(part);
-               container.removePart(0);
-               assertThat(container.getPart(0), is(part));
-       }
-
-       @Test
-       public void textOfContainerPartIsTextOfPartsConcatenated() {
-               container.add(createPartWithText("first"));
-               container.add(createPartWithText("second"));
-               assertThat(container.getText(), is("firstsecond"));
-       }
-
-       private Part createPartWithText(String text) {
-               Part part = mock(Part.class);
-               when(part.getText()).thenReturn(text);
-               return part;
-       }
-
-       @Test(expected = NoSuchElementException.class)
-       public void emptyContainerIteratorThrowsOnNext() {
-               container.iterator().next();
-       }
-
-       @Test
-       public void iteratorIteratesPartsRecursivelyInCorrectOrder() {
-               Part firstPart = mock(Part.class);
-               PartContainer secondPart = new PartContainer();
-               Part thirdPart = mock(Part.class);
-               Part nestedFirstPart = mock(Part.class);
-               Part nestedSecondPart = mock(Part.class);
-               secondPart.add(nestedFirstPart);
-               secondPart.add(nestedSecondPart);
-               container.add(firstPart);
-               container.add(secondPart);
-               container.add(thirdPart);
-               Iterator<Part> parts = container.iterator();
-               assertThat(parts.next(), is(firstPart));
-               assertThat(parts.next(), is(nestedFirstPart));
-               assertThat(parts.next(), is(nestedSecondPart));
-               assertThat(parts.next(), is(thirdPart));
-               assertThat(parts.hasNext(), is(false));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/text/PlainTextPartTest.java b/src/test/java/net/pterodactylus/sone/text/PlainTextPartTest.java
deleted file mode 100644 (file)
index 72161ab..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.pterodactylus.sone.text;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link PlainTextPart}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PlainTextPartTest {
-
-       private final PlainTextPart part = new PlainTextPart("text");
-
-       @Test
-       public void textIsRetainedCorrectly() {
-               assertThat(part.getText(), is("text"));
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForText() {
-           new PlainTextPart(null);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/text/SonePartTest.java b/src/test/java/net/pterodactylus/sone/text/SonePartTest.java
deleted file mode 100644 (file)
index 589acf7..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.pterodactylus.sone.text;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Sone;
-
-import org.hamcrest.MatcherAssert;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-/**
- * Unit test for {@link SonePart}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class SonePartTest {
-
-       private final Sone sone = mock(Sone.class);
-       private final SonePart part = new SonePart(sone);
-
-       @Test
-       public void soneIsRetainedCorrectly() {
-           assertThat(part.getSone(), is(sone));
-       }
-
-       @Test
-       public void textIsConstructedFromSonesNiceName() {
-           when(sone.getProfile()).thenReturn(mock(Profile.class));
-               when(sone.getName()).thenReturn("sone");
-               assertThat(part.getText(), is("sone"));
-       }
-
-       @Test(expected = NullPointerException.class)
-       public void nullIsNotAllowedForSone() {
-           new SonePart(null);
-       }
-
-}
index 6483171..0daf944 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.text;
 
+import static java.lang.String.format;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isIn;
@@ -49,17 +50,14 @@ public class SoneTextParserTest {
        public void testPlainText() throws IOException {
                /* check basic operation. */
                Iterable<Part> parts = soneTextParser.parse("Test.", null);
-               assertThat("Parts", parts, notNullValue());
                assertThat("Part Text", convertText(parts, PlainTextPart.class), is("Test."));
 
                /* check empty lines at start and end. */
                parts = soneTextParser.parse("\nTest.\n\n", null);
-               assertThat("Parts", parts, notNullValue());
                assertThat("Part Text", convertText(parts, PlainTextPart.class), is("Test."));
 
                /* check duplicate empty lines in the text. */
                parts = soneTextParser.parse("\nTest.\n\n\nTest.", null);
-               assertThat("Parts", parts, notNullValue());
                assertThat("Part Text", convertText(parts, PlainTextPart.class), is("Test.\n\nTest."));
        }
 
@@ -72,13 +70,13 @@ public class SoneTextParserTest {
        @Test
        public void freenetLinksHaveTheFreenetPrefixRemoved() {
                Iterable<Part> parts = soneTextParser.parse("freenet:KSK@gpl.txt", null);
-               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt|gpl.txt|gpl.txt]"));
+               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"));
        }
 
        @Test
        public void onlyTheFirstItemInALineIsPrefixedWithALineBreak() {
                Iterable<Part> parts = soneTextParser.parse("Text.\nKSK@gpl.txt and KSK@gpl.txt", null);
-               assertThat("Part Text", convertText(parts), is("Text.\n[KSK@gpl.txt|gpl.txt|gpl.txt] and [KSK@gpl.txt|gpl.txt|gpl.txt]"));
+               assertThat("Part Text", convertText(parts), is("Text.\n[KSK@gpl.txt|KSK@gpl.txt|gpl.txt] and [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"));
        }
 
        @Test
@@ -95,6 +93,13 @@ public class SoneTextParserTest {
        }
 
        @Test
+       public void soneAndPostCanBeParsedFromTheSameText() {
+               SoneTextParser parser = new SoneTextParser(new TestSoneProvider(), new TestPostProvider());
+               Iterable<Part> parts = parser.parse("Text sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU more text post://f3757817-b45a-497a-803f-9c5aafc10dc6 even more text", null);
+               assertThat("Part Text", convertText(parts), is("Text [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] more text [Post|f3757817-b45a-497a-803f-9c5aafc10dc6|text] even more text"));
+       }
+
+       @Test
        public void postLinkIsRenderedAsPlainTextIfPostIdIsTooShort() {
                Iterable<Part> parts = soneTextParser.parse("post://too-short", null);
                assertThat("Part Text", convertText(parts), is("post://too-short"));
@@ -116,26 +121,26 @@ public class SoneTextParserTest {
 
        @Test
        public void nameOfFreenetLinkDoesNotContainUrlParameters() {
-           Iterable<Part> parts = soneTextParser.parse("KSK@gpl.txt?max-size=12345", null);
-               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt?max-size=12345|gpl.txt|gpl.txt]"));
+               Iterable<Part> parts = soneTextParser.parse("KSK@gpl.txt?max-size=12345", null);
+               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt?max-size=12345|KSK@gpl.txt|gpl.txt]"));
        }
 
        @Test
        public void trailingSlashInFreenetLinkIsRemovedForName() {
                Iterable<Part> parts = soneTextParser.parse("KSK@gpl.txt/", null);
-               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt/|gpl.txt|gpl.txt]"));
+               assertThat("Part Text", convertText(parts), is("[KSK@gpl.txt/|KSK@gpl.txt/|gpl.txt]"));
        }
 
        @Test
        public void lastMetaStringOfFreenetLinkIsUsedAsName() {
                Iterable<Part> parts = soneTextParser.parse("CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING", null);
-               assertThat("Part Text", convertText(parts), is("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING|COPYING|COPYING]"));
+               assertThat("Part Text", convertText(parts), is("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING|CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING|COPYING]"));
        }
 
        @Test
        public void freenetLinkWithoutMetaStringsAndDocNameGetsFirstNineCharactersOfKeyAsName() {
                Iterable<Part> parts = soneTextParser.parse("CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8", null);
-               assertThat("Part Text", convertText(parts), is("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|CHK@qM1nm|CHK@qM1nm]"));
+               assertThat("Part Text", convertText(parts), is("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|CHK@qM1nm]"));
        }
 
        @Test
@@ -147,43 +152,43 @@ public class SoneTextParserTest {
        @Test
        public void httpsLinkHasItsPathsShortened() {
                Iterable<Part> parts = soneTextParser.parse("https://test.test/some-long-path/file.txt", null);
-               assertThat("Part Text", convertText(parts), is("[https://test.test/some-long-path/file.txt|test.test/…/file.txt|test.test/…/file.txt]"));
+               assertThat("Part Text", convertText(parts), is("[https://test.test/some-long-path/file.txt|https://test.test/some-long-path/file.txt|test.test/…/file.txt]"));
        }
 
        @Test
        public void httpLinksHaveTheirLastSlashRemoved() {
-           Iterable<Part> parts = soneTextParser.parse("http://test.test/test/", null);
-               assertThat("Part Text", convertText(parts), is("[http://test.test/test/|test.test/…|test.test/…]"));
+               Iterable<Part> parts = soneTextParser.parse("http://test.test/test/", null);
+               assertThat("Part Text", convertText(parts), is("[http://test.test/test/|http://test.test/test/|test.test/…]"));
        }
 
        @Test
        public void wwwPrefixIsRemovedForHostnameWithTwoDotsAndNoPath() {
                Iterable<Part> parts = soneTextParser.parse("http://www.test.test", null);
-               assertThat("Part Text", convertText(parts), is("[http://www.test.test|test.test|test.test]"));
+               assertThat("Part Text", convertText(parts), is("[http://www.test.test|http://www.test.test|test.test]"));
        }
 
        @Test
        public void wwwPrefixIsRemovedForHostnameWithTwoDotsAndAPath() {
                Iterable<Part> parts = soneTextParser.parse("http://www.test.test/test.html", null);
-               assertThat("Part Text", convertText(parts), is("[http://www.test.test/test.html|test.test/test.html|test.test/test.html]"));
+               assertThat("Part Text", convertText(parts), is("[http://www.test.test/test.html|http://www.test.test/test.html|test.test/test.html]"));
        }
 
        @Test
        public void hostnameIsKeptIntactIfNotBeginningWithWww() {
                Iterable<Part> parts = soneTextParser.parse("http://test.test.test/test.html", null);
-               assertThat("Part Text", convertText(parts), is("[http://test.test.test/test.html|test.test.test/test.html|test.test.test/test.html]"));
+               assertThat("Part Text", convertText(parts), is("[http://test.test.test/test.html|http://test.test.test/test.html|test.test.test/test.html]"));
        }
 
        @Test
        public void hostnameWithOneDotButNoSlashIsKeptIntact() {
                Iterable<Part> parts = soneTextParser.parse("http://test.test", null);
-               assertThat("Part Text", convertText(parts), is("[http://test.test|test.test|test.test]"));
+               assertThat("Part Text", convertText(parts), is("[http://test.test|http://test.test|test.test]"));
        }
 
        @Test
        public void urlParametersAreRemovedForHttpLinks() {
                Iterable<Part> parts = soneTextParser.parse("http://test.test?foo=bar", null);
-               assertThat("Part Text", convertText(parts), is("[http://test.test?foo=bar|test.test|test.test]"));
+               assertThat("Part Text", convertText(parts), is("[http://test.test?foo=bar|http://test.test?foo=bar|test.test]"));
        }
 
        @Test
@@ -201,35 +206,35 @@ public class SoneTextParserTest {
        @Test
        public void sskLinkWithoutContextIsNotTrusted() {
                Iterable<Part> parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", null);
-               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test|test]"));
+               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"));
        }
 
        @Test
        public void sskLinkWithContextWithoutSoneIsNotTrusted() {
                SoneTextParserContext context = new SoneTextParserContext(null);
                Iterable<Part> parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context);
-               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test|test]"));
+               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"));
        }
 
        @Test
        public void sskLinkWithContextWithDifferentSoneIsNotTrusted() {
                SoneTextParserContext context = new SoneTextParserContext(new IdOnlySone("DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU"));
                Iterable<Part> parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context);
-               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test|test]"));
+               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"));
        }
 
        @Test
        public void sskLinkWithContextWithCorrectSoneIsTrusted() {
                SoneTextParserContext context = new SoneTextParserContext(new IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"));
                Iterable<Part> parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context);
-               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|trusted|test|test]"));
+               assertThat("Part Text", convertText(parts), is("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|trusted|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"));
        }
 
        @Test
        public void uskLinkWithContextWithCorrectSoneIsTrusted() {
                SoneTextParserContext context = new SoneTextParserContext(new IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"));
                Iterable<Part> parts = soneTextParser.parse("USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0", context);
-               assertThat("Part Text", convertText(parts), is("[USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|trusted|test|test]"));
+               assertThat("Part Text", convertText(parts), is("[USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|trusted|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]"));
        }
 
        @SuppressWarnings("static-method")
@@ -237,18 +242,15 @@ public class SoneTextParserTest {
        public void testKSKLinks() throws IOException {
                /* check basic links. */
                Iterable<Part> parts = soneTextParser.parse("KSK@gpl.txt", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, FreenetLinkPart.class), is("[KSK@gpl.txt|gpl.txt|gpl.txt]"));
+               assertThat("Part Text", convertText(parts, FreenetLinkPart.class), is("[KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"));
 
                /* check embedded links. */
                parts = soneTextParser.parse("Link is KSK@gpl.txt\u200b.", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, FreenetLinkPart.class), is("Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\u200b."));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, FreenetLinkPart.class), is("Link is [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]\u200b."));
 
                /* check embedded links and line breaks. */
                parts = soneTextParser.parse("Link is KSK@gpl.txt\nKSK@test.dat\n", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, FreenetLinkPart.class), is("Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\n[KSK@test.dat|test.dat|test.dat]"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, FreenetLinkPart.class), is("Link is [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]\n[KSK@test.dat|KSK@test.dat|test.dat]"));
        }
 
        @SuppressWarnings({ "synthetic-access", "static-method" })
@@ -258,7 +260,6 @@ public class SoneTextParserTest {
 
                /* check basic links. */
                Iterable<Part> parts = soneTextParser.parse("Some text.\n\nLink to sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU and stuff.", null);
-               assertThat("Parts", parts, notNullValue());
                assertThat("Part Text", convertText(parts, PlainTextPart.class, SonePart.class), is("Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff."));
        }
 
@@ -269,52 +270,91 @@ public class SoneTextParserTest {
 
                /* check empty http links. */
                Iterable<Part> parts = soneTextParser.parse("Some text. Empty link: http:// – nice!", null);
-               assertThat("Parts", parts, notNullValue());
                assertThat("Part Text", convertText(parts, PlainTextPart.class), is("Some text. Empty link: http:// – nice!"));
        }
 
        @Test
        public void httpLinkWithoutParensEndsAtNextClosingParen() {
                Iterable<Part> parts = soneTextParser.parse("Some text (and a link: http://example.sone/abc) – nice!", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text (and a link: [http://example.sone/abc|example.sone/abc|example.sone/abc]) – nice!"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text (and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]) – nice!"));
        }
 
        @Test
        public void uskLinkEndsAtFirstNonNumericNonSlashCharacterAfterVersionNumber() {
                Iterable<Part> parts = soneTextParser.parse("Some link (USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0). Nice", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts), is("Some link ([USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test|test]). Nice"));
+               assertThat("Part Text", convertText(parts), is("Some link ([USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]). Nice"));
        }
 
        @Test
        public void httpLinkWithOpenedAndClosedParensEndsAtNextClosingParen() {
                Iterable<Part> parts = soneTextParser.parse("Some text (and a link: http://example.sone/abc_(def)) – nice!", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text (and a link: [http://example.sone/abc_(def)|example.sone/abc_(def)|example.sone/abc_(def)]) – nice!"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text (and a link: [http://example.sone/abc_(def)|http://example.sone/abc_(def)|example.sone/abc_(def)]) – nice!"));
        }
 
        @Test
        public void punctuationIsIgnoredAtEndOfLinkBeforeWhitespace() {
-               SoneTextParser soneTextParser = new SoneTextParser(null, null);
                Iterable<Part> parts = soneTextParser.parse("Some text and a link: http://example.sone/abc. Nice!", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|example.sone/abc|example.sone/abc]. Nice!"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]. Nice!"));
        }
 
        @Test
        public void multiplePunctuationCharactersAreIgnoredAtEndOfLinkBeforeWhitespace() {
                Iterable<Part> parts = soneTextParser.parse("Some text and a link: http://example.sone/abc... Nice!", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|example.sone/abc|example.sone/abc]... Nice!"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]... Nice!"));
        }
 
        @Test
        public void commasAreIgnoredAtEndOfLinkBeforeWhitespace() {
-               SoneTextParser soneTextParser = new SoneTextParser(null, null);
                Iterable<Part> parts = soneTextParser.parse("Some text and a link: http://example.sone/abc, nice!", null);
-               assertThat("Parts", parts, notNullValue());
-               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|example.sone/abc|example.sone/abc], nice!"));
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc], nice!"));
+       }
+
+       @Test
+       public void exclamationMarksAreIgnoredAtEndOfLinkBeforeWhitespace() {
+               Iterable<Part> parts = soneTextParser.parse("A link: http://example.sone/abc!", null);
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("A link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]!"));
+       }
+
+       @Test
+       public void questionMarksAreIgnoredAtEndOfLinkBeforeWhitespace() {
+               Iterable<Part> parts = soneTextParser.parse("A link: http://example.sone/abc?", null);
+               assertThat("Part Text", convertText(parts, PlainTextPart.class, LinkPart.class), is("A link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]?"));
+       }
+
+       @Test
+       public void correctFreemailAddressIsLinkedToCorrectly() {
+               Iterable<Part> parts = soneTextParser.parse("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null);
+               assertThat("Part Text", convertText(parts), is("Mail me at [Freemail|sone|t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra|nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI]!"));
+       }
+
+       @Test
+       public void freemailAddressWithInvalidFreemailIdIsParsedAsText() {
+               Iterable<Part> parts = soneTextParser.parse("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqr8.freemail!", null);
+               assertThat("Part Text", convertText(parts), is("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqr8.freemail!"));
+       }
+
+       @Test
+       public void freemailAddressWithInvalidSizedFreemailIdIsParsedAsText() {
+               Iterable<Part> parts = soneTextParser.parse("Mail me at sone@4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null);
+               assertThat("Part Text", convertText(parts), is("Mail me at sone@4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!"));
+       }
+
+       @Test
+       public void freemailAddressWithoutLocalPartIsParsedAsText() {
+               Iterable<Part> parts = soneTextParser.parse("     @t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null);
+               assertThat("Part Text", convertText(parts), is("     @t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!"));
+       }
+
+       @Test
+       public void correctFreemailAddressIsParsedCorrectly() {
+               Iterable<Part> parts = soneTextParser.parse("sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail", null);
+               assertThat("Part Text", convertText(parts), is("[Freemail|sone|t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra|nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI]"));
+       }
+
+       @Test
+       public void localPartOfFreemailAddressCanContainLettersDigitsMinusDotUnderscore() {
+               Iterable<Part> parts = soneTextParser.parse("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail", null);
+               assertThat("Part Text", convertText(parts), is("[Freemail|ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._|t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra|nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI]"));
        }
 
        /**
@@ -339,7 +379,10 @@ public class SoneTextParserTest {
                                text.append(((PlainTextPart) part).getText());
                        } else if (part instanceof FreenetLinkPart) {
                                FreenetLinkPart freenetLinkPart = (FreenetLinkPart) part;
-                               text.append('[').append(freenetLinkPart.getLink()).append('|').append(freenetLinkPart.isTrusted() ? "trusted|" : "").append(freenetLinkPart.getTitle()).append('|').append(freenetLinkPart.getText()).append(']');
+                               text.append('[').append(freenetLinkPart.getLink()).append('|').append(freenetLinkPart.getTrusted() ? "trusted|" : "").append(freenetLinkPart.getTitle()).append('|').append(freenetLinkPart.getText()).append(']');
+                       } else if (part instanceof FreemailPart) {
+                               FreemailPart freemailPart = (FreemailPart) part;
+                               text.append(format("[Freemail|%s|%s|%s]", freemailPart.getEmailLocalPart(), freemailPart.getFreemailId(), freemailPart.getIdentityId()));
                        } else if (part instanceof LinkPart) {
                                LinkPart linkPart = (LinkPart) part;
                                text.append('[').append(linkPart.getLink()).append('|').append(linkPart.getTitle()).append('|').append(linkPart.getText()).append(']');
index b2d078e..687afa3 100644 (file)
@@ -4,7 +4,7 @@ import static net.pterodactylus.sone.utils.IntegerRangePredicate.range;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 
-import net.pterodactylus.sone.TestUtil;
+import net.pterodactylus.sone.test.TestUtil;
 
 import org.junit.Test;
 
diff --git a/src/test/java/net/pterodactylus/sone/web/AboutPageTest.java b/src/test/java/net/pterodactylus/sone/web/AboutPageTest.java
deleted file mode 100644 (file)
index df41e61..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link AboutPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class AboutPageTest extends WebPageTest {
-
-       private final String version = "0.1.2";
-       private final int year = 1234;
-       private final String homepage = "home://page";
-       private final AboutPage page = new AboutPage(template, webInterface, version, year, homepage);
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("about.html"));
-       }
-
-       @Test
-       public void pageSetsCorrectVersionInTemplateContext() throws Exception {
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("version"), is((Object) version));
-       }
-
-       @Test
-       public void pageSetsCorrectHomepageInTemplateContext() throws Exception {
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("homepage"), is((Object) homepage));
-       }
-
-       @Test
-       public void pageSetsCorrectYearInTemplateContext() throws Exception {
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("year"), is((Object) year));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/BookmarkPageTest.java b/src/test/java/net/pterodactylus/sone/web/BookmarkPageTest.java
deleted file mode 100644 (file)
index ddba2c5..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.util.web.Method;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link BookmarkPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class BookmarkPageTest extends WebPageTest {
-
-       private final BookmarkPage page = new BookmarkPage(template, webInterface);
-
-       @Test
-       public void pathIsSetCorrectly() {
-               assertThat(page.getPath(), is("bookmark.html"));
-       }
-
-       @Test
-       public void getRequestDoesNotBookmarkAnythingAndDoesNotRedirect() throws Exception {
-               page.processTemplate(freenetRequest, templateContext);
-               verify(core, never()).bookmarkPost(any(Post.class));
-       }
-
-       @Test
-       public void postIsBookmarkedCorrectly() throws Exception {
-               setupRequest();
-               Post post = mock(Post.class);
-               addPost("post-id", post);
-               expectedException.expect(redirectsTo("return-page.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).bookmarkPost(post);
-               }
-       }
-
-       private void setupRequest() {
-               request("", Method.POST);
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("returnPage", "return-page.html");
-       }
-
-       @Test
-       public void nonExistentPostIsNotBookmarked() throws Exception {
-               setupRequest();
-               addPost("post-id", null);
-               expectedException.expect(redirectsTo("return-page.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core, never()).bookmarkPost(any(Post.class));
-               }
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/BookmarksPageTest.java b/src/test/java/net/pterodactylus/sone/web/BookmarksPageTest.java
deleted file mode 100644 (file)
index 46b0a18..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException;
-import net.pterodactylus.util.collection.Pagination;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link BookmarksPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class BookmarksPageTest extends WebPageTest {
-
-       private final BookmarksPage page = new BookmarksPage(template, webInterface);
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("bookmarks.html"));
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void pageSetsCorrectPostsInTemplateContext() throws RedirectException {
-               Post post1 = createPost(true, 3000L);
-               Post post2 = createPost(true, 1000L);
-               Post post3 = createPost(true, 2000L);
-               Set<Post> bookmarkedPosts = createBookmarkedPosts(post1, post2, post3);
-               when(core.getBookmarkedPosts()).thenReturn(bookmarkedPosts);
-               when(core.getPreferences().getPostsPerPage()).thenReturn(5);
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat((Collection<Post>) templateContext.get("posts"), contains(post1, post3, post2));
-               assertThat(((Pagination<Post>) templateContext.get("pagination")).getItems(), contains(post1, post3, post2));
-               assertThat(((Boolean) templateContext.get("postsNotLoaded")), is(false));
-       }
-
-       private Set<Post> createBookmarkedPosts(Post post1, Post post2, Post post3) {
-               Set<Post> bookmarkedPosts = new HashSet<>();
-               bookmarkedPosts.add(post1);
-               bookmarkedPosts.add(post2);
-               bookmarkedPosts.add(post3);
-               return bookmarkedPosts;
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void notLoadedPostsAreNotIncludedButAFlagIsSet() throws RedirectException {
-               Post post1 = createPost(true, 1000L);
-               Post post2 = createPost(true, 3000L);
-               Post post3 = createPost(false, 2000L);
-               Set<Post> bookmarkedPosts = createBookmarkedPosts(post1, post2, post3);
-               when(core.getBookmarkedPosts()).thenReturn(bookmarkedPosts);
-               when(core.getPreferences().getPostsPerPage()).thenReturn(5);
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat((Collection<Post>) templateContext.get("posts"), contains(post2, post1));
-               assertThat(((Pagination<Post>) templateContext.get("pagination")).getItems(), contains(post2, post1));
-               assertThat(((Boolean) templateContext.get("postsNotLoaded")), is(true));
-       }
-
-       private Post createPost(boolean postLoaded, long time) {
-               Post post = mock(Post.class);
-               when(post.isLoaded()).thenReturn(postLoaded);
-               when(post.getTime()).thenReturn(time);
-               return post;
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.java b/src/test/java/net/pterodactylus/sone/web/CreateAlbumPageTest.java
deleted file mode 100644 (file)
index da0ec0c..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static net.pterodactylus.util.web.Method.POST;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Answers.RETURNS_DEEP_STUBS;
-import static org.mockito.Answers.RETURNS_SELF;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-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.Dirty;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link CreateAlbumPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateAlbumPageTest extends WebPageTest {
-
-       private final CreateAlbumPage page = new CreateAlbumPage(template, webInterface);
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("createAlbum.html"));
-       }
-
-       @Test
-       public void getRequestShowsTemplate() throws Exception {
-               page.processTemplate(freenetRequest, templateContext);
-       }
-
-       @Test
-       public void missingNameResultsInAttributeSetInTemplateContext() throws Exception {
-               request("", POST);
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("nameMissing"), is((Object) true));
-       }
-
-       @Test
-       public void titleAndDescriptionAreSetCorrectlyOnTheAlbum() throws Exception {
-               request("", POST);
-               Album parentAlbum = createAlbum("parent-id");
-               when(core.getAlbum("parent-id")).thenReturn(parentAlbum);
-               Album newAlbum = createAlbum("album-id");
-               when(core.createAlbum(currentSone, parentAlbum)).thenReturn(newAlbum);
-               addHttpRequestParameter("name", "new name");
-               addHttpRequestParameter("description", "new description");
-               addHttpRequestParameter("parent", "parent-id");
-               expectedException.expect(redirectsTo("imageBrowser.html?album=album-id"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(newAlbum).modify();
-                       verify(newAlbum.modify()).setTitle("new name");
-                       verify(newAlbum.modify()).setDescription("new description");
-                       verify(newAlbum.modify()).update();
-                       verify(core).touchConfiguration();
-               }
-       }
-
-       private Album createAlbum(String albumId) {
-               Album newAlbum = mock(Album.class, RETURNS_DEEP_STUBS);
-               when(newAlbum.getId()).thenReturn(albumId);
-               Modifier albumModifier = mock(Modifier.class, RETURNS_SELF);
-               when(newAlbum.modify()).thenReturn(albumModifier);
-               when(albumModifier.update()).thenReturn(newAlbum);
-               return newAlbum;
-       }
-
-       @Test
-       public void rootAlbumIsUsedIfNoParentIsSpecified() throws Exception {
-               request("", POST);
-               Album parentAlbum = createAlbum("root-id");
-               when(currentSone.getRootAlbum()).thenReturn(parentAlbum);
-               Album newAlbum = createAlbum("album-id");
-               when(core.createAlbum(currentSone, parentAlbum)).thenReturn(newAlbum);
-               addHttpRequestParameter("name", "new name");
-               addHttpRequestParameter("description", "new description");
-               expectedException.expect(redirectsTo("imageBrowser.html?album=album-id"));
-               page.processTemplate(freenetRequest, templateContext);
-       }
-
-       @Test
-       @Dirty("that exception can never happen")
-       public void emptyAlbumTitleRedirectsToErrorPage() throws Exception {
-               request("", POST);
-               Album parentAlbum = createAlbum("root-id");
-               when(currentSone.getRootAlbum()).thenReturn(parentAlbum);
-               Album newAlbum = createAlbum("album-id");
-               when(core.createAlbum(currentSone, parentAlbum)).thenReturn(newAlbum);
-               when(newAlbum.modify().update()).thenThrow(AlbumTitleMustNotBeEmpty.class);
-               addHttpRequestParameter("name", "new name");
-               addHttpRequestParameter("description", "new description");
-               expectedException.expect(redirectsTo("emptyAlbumTitle.html"));
-               page.processTemplate(freenetRequest, templateContext);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreatePostPageTest.java b/src/test/java/net/pterodactylus/sone/web/CreatePostPageTest.java
deleted file mode 100644 (file)
index 2a91086..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.util.web.Method.POST;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import net.pterodactylus.sone.data.Sone;
-
-import com.google.common.base.Optional;
-import org.junit.Test;
-
-/**
- * Unit test for {@link CreatePostPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreatePostPageTest extends WebPageTest {
-
-       private final CreatePostPage page = new CreatePostPage(template, webInterface);
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("createPost.html"));
-       }
-
-       @Test
-       public void returnPageIsSetInTemplateContext() throws Exception {
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("returnPage"), is((Object) "returnPage.html"));
-       }
-
-       @Test
-       public void postIsCreatedCorrectly() throws Exception {
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               addHttpRequestParameter("text", "post text");
-               request("", POST);
-               expectedException.expect(WebTestUtils.redirectsTo("returnPage.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createPost(currentSone, Optional.<Sone>absent(), "post text");
-               }
-       }
-
-       @Test
-       public void creatingAnEmptyPostIsDenied() throws Exception {
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               addHttpRequestParameter("text", "   ");
-               request("", POST);
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.get("errorTextEmpty"), is((Object) true));
-       }
-
-       @Test
-       public void aSenderCanBeSelected() throws Exception {
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               addHttpRequestParameter("text", "post text");
-               addHttpRequestParameter("sender", "sender-id");
-               Sone sender = mock(Sone.class);
-               addLocalSone("sender-id", sender);
-               request("", POST);
-               expectedException.expect(WebTestUtils.redirectsTo("returnPage.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createPost(sender, Optional.<Sone>absent(), "post text");
-               }
-       }
-
-       @Test
-       public void aRecipientCanBeSelected() throws Exception {
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               addHttpRequestParameter("text", "post text");
-               addHttpRequestParameter("recipient", "recipient-id");
-               Sone recipient = mock(Sone.class);
-               addSone("recipient-id", recipient);
-               request("", POST);
-               expectedException.expect(WebTestUtils.redirectsTo("returnPage.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createPost(currentSone, Optional.of(recipient), "post text");
-               }
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.java b/src/test/java/net/pterodactylus/sone/web/CreateReplyPageTest.java
deleted file mode 100644 (file)
index 996f5c0..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static net.pterodactylus.util.web.Method.GET;
-import static net.pterodactylus.util.web.Method.POST;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-
-import org.junit.Test;
-
-/**
- * Unit test for {@link CreateReplyPageTest}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateReplyPageTest extends WebPageTest {
-
-       private final CreateReplyPage page = new CreateReplyPage(template, webInterface);
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("createReply.html"));
-       }
-
-       @Test
-       public void replyIsCreatedCorrectly() throws Exception {
-               request("", POST);
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("text", "some text");
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               Post post = mock(Post.class);
-               addPost("post-id", post);
-               expectedException.expect(redirectsTo("returnPage.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createReply(currentSone, post, "some text");
-               }
-       }
-
-       @Test
-       public void replyIsCreatedWithCorrectSender() throws Exception {
-               request("", POST);
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("text", "some text");
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               addHttpRequestParameter("sender", "sender-id");
-               Sone sender = mock(Sone.class);
-               addLocalSone("sender-id", sender);
-               Post post = mock(Post.class);
-               addPost("post-id", post);
-               expectedException.expect(redirectsTo("returnPage.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createReply(sender, post, "some text");
-               }
-       }
-
-       @Test
-       public void emptyTextSetsVariableInTemplateContext() throws Exception {
-               request("", POST);
-               addPost("post-id", mock(Post.class));
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("text", "   ");
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(templateContext.<Boolean>get("errorTextEmpty", Boolean.class), is(true));
-               verifyParametersAreCopied("");
-               verify(core, never()).createReply(any(Sone.class), any(Post.class), anyString());
-       }
-
-       private void verifyParametersAreCopied(String text) {
-               assertThat(templateContext.<String>get("postId", String.class), is("post-id"));
-               assertThat(templateContext.<String>get("text", String.class), is(text));
-               assertThat(templateContext.<String>get("returnPage", String.class), is("returnPage.html"));
-       }
-
-       @Test
-       public void userIsRedirectIfPostDoesNotExist() throws Exception {
-               request("", POST);
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("text", "some text");
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               expectedException.expect(redirectsTo("noPermission.html"));
-               page.processTemplate(freenetRequest, templateContext);
-       }
-
-       @Test
-       public void getRequestServesTemplateAndStoresParameters() throws Exception {
-               request("", GET);
-               addHttpRequestParameter("post", "post-id");
-               addHttpRequestParameter("text", "some text");
-               addHttpRequestParameter("returnPage", "returnPage.html");
-               page.processTemplate(freenetRequest, templateContext);
-               verifyParametersAreCopied("some text");
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/CreateSonePageTest.java b/src/test/java/net/pterodactylus/sone/web/CreateSonePageTest.java
deleted file mode 100644 (file)
index aab40c9..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static net.pterodactylus.util.web.Method.POST;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-
-import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/**
- * Unit test for {@link CreateSonePage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class CreateSonePageTest extends WebPageTest {
-
-       private final CreateSonePage page = new CreateSonePage(template, webInterface);
-       private final Sone[] localSones = { createSone("local-sone1"), createSone("local-sone2"), createSone("local-sone3") };
-       private final OwnIdentity[] ownIdentities = {
-                       createOwnIdentity("own-id-1", "Sone"),
-                       createOwnIdentity("own-id-2", "Test", "Foo"),
-                       createOwnIdentity("own-id-3"),
-                       createOwnIdentity("own-id-4", "Sone")
-       };
-
-       @Test
-       public void pageReturnsCorrectPath() {
-               assertThat(page.getPath(), is("createSone.html"));
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void getRequestStoresListOfIdentitiesInTemplateContext() throws Exception {
-               addDefaultLocalSones();
-               addDefaultOwnIdentities();
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat((Collection<Sone>) templateContext.get("sones"), contains(localSones[0], localSones[1], localSones[2]));
-               assertThat((Collection<OwnIdentity>) templateContext.get("identitiesWithoutSone"), contains(ownIdentities[1], ownIdentities[2]));
-       }
-
-       private void addDefaultLocalSones() {
-               addLocalSone("local-sone3", localSones[2]);
-               addLocalSone("local-sone1", localSones[0]);
-               addLocalSone("local-sone2", localSones[1]);
-       }
-
-       private void addDefaultOwnIdentities() {
-               addOwnIdentity(ownIdentities[2]);
-               addOwnIdentity(ownIdentities[0]);
-               addOwnIdentity(ownIdentities[3]);
-               addOwnIdentity(ownIdentities[1]);
-       }
-
-       private Sone createSone(String id) {
-               Sone sone = mock(Sone.class);
-               when(sone.getId()).thenReturn(id);
-               when(sone.getProfile()).thenReturn(new Profile(sone));
-               return sone;
-       }
-
-       private OwnIdentity createOwnIdentity(String id, final String... contexts) {
-               OwnIdentity ownIdentity = mock(OwnIdentity.class);
-               when(ownIdentity.getId()).thenReturn(id);
-               when(ownIdentity.getNickname()).thenReturn(id);
-               when(ownIdentity.getContexts()).thenReturn(new HashSet<>(Arrays.asList(contexts)));
-               when(ownIdentity.hasContext(anyString())).thenAnswer(new Answer<Boolean>() {
-                       @Override
-                       public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                               return Arrays.asList(contexts).contains(invocation.<String>getArgument(0));
-                       }
-               });
-               return ownIdentity;
-       }
-
-       @Test
-       public void soneIsCreatedAndLoggedIn() throws Exception {
-               addDefaultLocalSones();
-               addDefaultOwnIdentities();
-               addHttpRequestParameter("identity", "own-id-3");
-               request("", POST);
-               Sone newSone = mock(Sone.class);
-               when(core.createSone(ownIdentities[2])).thenReturn(newSone);
-               expectedException.expect(redirectsTo("index.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createSone(ownIdentities[2]);
-                       verify(webInterface).setCurrentSone(toadletContext, newSone);
-               }
-       }
-
-       @Test
-       public void onInvalidIdentityIdFlagIsStoredInTemplateContext() throws Exception {
-               addDefaultLocalSones();
-               addDefaultOwnIdentities();
-               addHttpRequestParameter("identity", "own-id-invalid");
-               request("", POST);
-               page.processTemplate(freenetRequest, templateContext);
-               assertThat(((Boolean) templateContext.get("errorNoIdentity")), is(true));
-       }
-
-       @Test
-       public void ifSoneIsNotCreatedUserIsStillRedirectedToIndex() throws Exception {
-               addDefaultLocalSones();
-               addDefaultOwnIdentities();
-               addHttpRequestParameter("identity", "own-id-3");
-               request("", POST);
-               when(core.createSone(ownIdentities[2])).thenReturn(null);
-               expectedException.expect(redirectsTo("index.html"));
-               try {
-                       page.processTemplate(freenetRequest, templateContext);
-               } finally {
-                       verify(core).createSone(ownIdentities[2]);
-                       verify(webInterface).setCurrentSone(toadletContext, null);
-               }
-       }
-
-       @Test
-       public void doNotShowCreateSoneInMenuIfFullAccessRequiredButClientHasNoFullAccess() {
-               when(core.getPreferences().isRequireFullAccess()).thenReturn(true);
-               when(toadletContext.isAllowedFullAccess()).thenReturn(false);
-               assertThat(page.isEnabled(toadletContext), is(false));
-       }
-
-       @Test
-       public void showCreateSoneInMenuIfNotLoggedInAndClientHasFullAccess() {
-               when(core.getPreferences().isRequireFullAccess()).thenReturn(true);
-               when(toadletContext.isAllowedFullAccess()).thenReturn(true);
-               unsetCurrentSone();
-               assertThat(page.isEnabled(toadletContext), is(true));
-       }
-
-       @Test
-       public void showCreateSoneInMenuIfNotLoggedIn() {
-               unsetCurrentSone();
-               assertThat(page.isEnabled(toadletContext), is(true));
-       }
-
-       @Test
-       public void showCreateSoneInMenuIfLoggedInAndASingleSoneExists() {
-               addLocalSone("local-sone", mock(Sone.class));
-               assertThat(page.isEnabled(toadletContext), is(true));
-       }
-
-       @Test
-       public void doNotShowCreateSoneInMenuIfLoggedInAndMoreLocalSonesExists() {
-               addLocalSone("local-sone1", mock(Sone.class));
-               addLocalSone("local-sone2", mock(Sone.class));
-               assertThat(page.isEnabled(toadletContext), is(false));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/DeleteReplyPageTest.java b/src/test/java/net/pterodactylus/sone/web/DeleteReplyPageTest.java
deleted file mode 100644 (file)
index 88c87a1..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static net.pterodactylus.util.web.Method.POST;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.PostReply;
-
-import com.google.common.base.Optional;
-import org.junit.Test;
-
-/**
- * Unit test for {@link DeleteReplyPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DeleteReplyPageTest extends WebPageTest {
-
-       private final DeleteReplyPage page = new DeleteReplyPage(template, webInterface);
-
-       @Test
-       public void tryingToDeleteAReplyWithAnInvalidIdResultsInNoPermissionPage() throws Exception {
-               request("", POST);
-               when(httpRequest.getPartAsStringFailsafe(eq("reply"), anyInt())).thenReturn("id");
-               when(webInterface.getCore().getPostReply("id")).thenReturn(Optional.<PostReply>absent());
-               expectedException.expect(redirectsTo("noPermission.html"));
-               page.processTemplate(freenetRequest, templateContext);
-       }
-
-}
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 e110969..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() {
-               when(webInterface.getCore().getPreferences().getPostsPerPage()).thenReturn(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/UploadImagePageTest.java b/src/test/java/net/pterodactylus/sone/web/UploadImagePageTest.java
deleted file mode 100644 (file)
index 3b8258e..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.pterodactylus.sone.web;
-
-import static net.pterodactylus.sone.web.WebTestUtils.redirectsTo;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.util.web.Method;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link UploadImagePageTest}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class UploadImagePageTest extends WebPageTest {
-
-       private final UploadImagePage uploadImagePage = new UploadImagePage(template, webInterface);
-
-       private final Album parentAlbum = mock(Album.class);
-
-       @Before
-       public void setupParentAlbum() {
-               when(core.getAlbum("parent-id")).thenReturn(parentAlbum);
-               when(parentAlbum.getSone()).thenReturn(currentSone);
-       }
-
-       @Test
-       public void uploadingAnImageWithoutTitleRedirectsToEmptyImageTitlePage() throws Exception {
-               request("", Method.POST);
-               when(httpRequest.getPartAsStringFailsafe(eq("parent"), anyInt())).thenReturn("parent-id");
-               when(httpRequest.getPartAsStringFailsafe(eq("title"), anyInt())).thenReturn("  ");
-               expectedException.expect(redirectsTo("emptyImageTitle.html"));
-               uploadImagePage.processTemplate(freenetRequest, templateContext);
-       }
-
-}
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 3e2df88..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-package net.pterodactylus.sone.web;
-
-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.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.core.UpdateChecker;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-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 freenet.clients.http.ToadletContext;
-import freenet.support.api.HTTPRequest;
-
-import com.google.common.base.Optional;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.rules.ExpectedException;
-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 Core core = webInterface.getCore();
-
-       protected final Sone currentSone = mock(Sone.class);
-
-       protected final TemplateContext templateContext = new TemplateContext();
-       protected final HTTPRequest httpRequest = mock(HTTPRequest.class);
-       protected final FreenetRequest freenetRequest = mock(FreenetRequest.class);
-       protected final ToadletContext toadletContext = mock(ToadletContext.class);
-
-       private final Set<OwnIdentity> ownIdentities = new HashSet<>();
-       private final List<Sone> localSones = new ArrayList<>();
-
-       @Before
-       public final void setupFreenetRequest() {
-               when(freenetRequest.getToadletContext()).thenReturn(toadletContext);
-               when(freenetRequest.getHttpRequest()).thenReturn(httpRequest);
-               when(httpRequest.getPartAsStringFailsafe(anyString(), anyInt())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               return "";
-                       }
-               });
-       }
-
-       @Before
-       public final void setupCore() {
-               UpdateChecker updateChecker = mock(UpdateChecker.class);
-               when(core.getUpdateChecker()).thenReturn(updateChecker);
-               when(core.getLocalSone(anyString())).thenReturn(null);
-               when(core.getLocalSones()).thenReturn(localSones);
-               when(core.getSone(anyString())).thenReturn(Optional.<Sone>absent());
-               when(core.getPost(anyString())).thenReturn(Optional.<Post>absent());
-       }
-
-       @Before
-       public final void setupIdentityManager() {
-               when(core.getIdentityManager().getAllOwnIdentities()).thenReturn(ownIdentities);
-       }
-
-       @Before
-       public final void setupWebInterface() {
-               when(webInterface.getCurrentSone(toadletContext)).thenReturn(currentSone);
-               when(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(currentSone);
-               when(webInterface.getNotifications(currentSone)).thenReturn(new ArrayList<Notification>());
-       }
-
-       protected void unsetCurrentSone() {
-               when(webInterface.getCurrentSone(toadletContext)).thenReturn(null);
-               when(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(null);
-       }
-
-       protected void request(String uri, Method method) {
-               try {
-                       when(freenetRequest.getUri()).thenReturn(new URI(uri));
-               } catch (URISyntaxException e) {
-                       throw new RuntimeException(e);
-               }
-               when(freenetRequest.getMethod()).thenReturn(method);
-       }
-
-       protected void addHttpRequestParameter(String name, final String value) {
-               when(httpRequest.getPartAsStringFailsafe(eq(name), anyInt())).thenAnswer(new Answer<String>() {
-                       @Override
-                       public String answer(InvocationOnMock invocation) throws Throwable {
-                               int maxLength = invocation.getArgument(1);
-                               return value.substring(0, Math.min(maxLength, value.length()));
-                       }
-               });
-       }
-
-       protected void addPost(String postId, Post post) {
-               when(core.getPost(postId)).thenReturn(Optional.fromNullable(post));
-       }
-
-       protected void addSone(String soneId, Sone sone) {
-               when(core.getSone(eq(soneId))).thenReturn(Optional.fromNullable(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);
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java b/src/test/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.java
deleted file mode 100644 (file)
index d7c2053..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * © 2013 xplosion interactive
- */
-
-package net.pterodactylus.sone.web.ajax;
-
-import static com.google.common.base.Optional.of;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.junit.Assert.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.FreenetRequest;
-
-import freenet.clients.http.HTTPRequestImpl;
-import freenet.support.api.HTTPRequest;
-import org.junit.Test;
-
-/**
- * Tests for {@link BookmarkAjaxPage}.
- *
- * @author <a href="mailto:d.roden@xplosion.de">David Roden</a>
- */
-public class BookmarkAjaxPageTest {
-
-       @Test
-       public void testBookmarkingExistingPost() throws URISyntaxException {
-               /* create mocks. */
-               Core core = mock(Core.class);
-               Post post = mock(Post.class);
-               when(core.getPost("abc")).thenReturn(of(post));
-               WebInterface webInterface = mock(WebInterface.class);
-               when(webInterface.getCore()).thenReturn(core);
-               HTTPRequest httpRequest = new HTTPRequestImpl(new URI("/ajax/bookmark.ajax?post=abc"), "GET");
-               FreenetRequest request = mock(FreenetRequest.class);
-               when(request.getHttpRequest()).thenReturn(httpRequest);
-
-               /* create JSON page. */
-               BookmarkAjaxPage bookmarkAjaxPage = new BookmarkAjaxPage(webInterface);
-               JsonReturnObject jsonReturnObject = bookmarkAjaxPage.createJsonObject(request);
-
-               /* verify response. */
-               assertThat(jsonReturnObject, notNullValue());
-               assertThat(jsonReturnObject.isSuccess(), is(true));
-
-               /* verify behaviour. */
-               verify(core).bookmarkPost(post);
-       }
-
-       @Test
-       public void testBookmarkingMissingPost() throws URISyntaxException {
-               /* create mocks. */
-               Core core = mock(Core.class);
-               WebInterface webInterface = mock(WebInterface.class);
-               when(webInterface.getCore()).thenReturn(core);
-               HTTPRequest httpRequest = new HTTPRequestImpl(new URI("/ajax/bookmark.ajax"), "GET");
-               FreenetRequest request = mock(FreenetRequest.class);
-               when(request.getHttpRequest()).thenReturn(httpRequest);
-
-               /* create JSON page. */
-               BookmarkAjaxPage bookmarkAjaxPage = new BookmarkAjaxPage(webInterface);
-               JsonReturnObject jsonReturnObject = bookmarkAjaxPage.createJsonObject(request);
-
-               /* verify response. */
-               assertThat(jsonReturnObject, notNullValue());
-               assertThat(jsonReturnObject.isSuccess(), is(false));
-               assertThat(((JsonErrorReturnObject) jsonReturnObject).getError(), is("invalid-post-id"));
-
-               /* verify behaviour. */
-               verify(core, never()).bookmarkPost(any(Post.class));
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.java b/src/test/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.java
deleted file mode 100644 (file)
index e1ab857..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package net.pterodactylus.sone.web.ajax;
-
-import static java.lang.System.currentTimeMillis;
-import static net.pterodactylus.sone.web.ajax.GetTimesAjaxPage.getTime;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage.Time;
-
-import org.junit.Test;
-import org.mockito.Mockito;
-
-/**
- * Unit test for {@link GetTimesAjaxPage}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class GetTimesAjaxPageTest {
-
-       private final WebInterface webInterface = Mockito.mock(WebInterface.class, RETURNS_DEEP_STUBS);
-
-       @Test
-       public void timestampInTheFutureIsTranslatedCorrectly() {
-               when(webInterface.getL10n().getString("View.Time.InTheFuture")).thenReturn("in the future");
-               Time time = getTime(webInterface, currentTimeMillis() + 100);
-               assertThat(time.getText(), is("in the future"));
-       }
-
-       @Test
-       public void timestampAFewSecondsAgoIsTranslatedCorrectly() {
-               when(webInterface.getL10n().getString("View.Time.AFewSecondsAgo")).thenReturn("a few seconds ago");
-               Time time = getTime(webInterface, currentTimeMillis() - 1000);
-               assertThat(time.getText(), is("a few seconds ago"));
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt
new file mode 100644 (file)
index 0000000..776d0c9
--- /dev/null
@@ -0,0 +1,201 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.base.Ticker
+import com.google.common.io.ByteStreams
+import freenet.keys.FreenetURI
+import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
+import net.pterodactylus.sone.test.capture
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.TimeUnit
+
+/**
+ * Unit test for [DefaultElementLoaderTest].
+ */
+class DefaultElementLoaderTest {
+
+       companion object {
+               private const val IMAGE_ID = "KSK@gpl.png"
+               private val freenetURI = FreenetURI(IMAGE_ID)
+               private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
+               private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
+               private const val textKey = "KSK@gpl.html"
+               private val sizeOkay = 2097152L
+               private val sizeNotOkay = sizeOkay + 1
+       }
+
+       private val freenetInterface = mock<FreenetInterface>()
+       private val ticker = mock<Ticker>()
+       private val elementLoader = DefaultElementLoader(freenetInterface, ticker)
+       private val callback = capture<BackgroundFetchCallback>()
+
+       @Test
+       fun `image loader starts request for link that is not known`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), any<BackgroundFetchCallback>())
+       }
+
+       @Test
+       fun `element loader only starts request once`() {
+               elementLoader.loadElement(IMAGE_ID)
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), any<BackgroundFetchCallback>())
+       }
+
+       @Test
+       fun `element loader returns loading element on first call`() {
+               assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true))
+       }
+
+       @Test
+       fun `element loader does not cancel on image mime type with 2 mib size`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeOkay), `is`(false))
+       }
+
+       @Test
+       fun `element loader does cancel on image mime type with more than 2 mib size`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "image/png", sizeNotOkay), `is`(true))
+       }
+
+       @Test
+       fun `element loader does cancel on audio mime type`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), `is`(true))
+       }
+
+       @Test
+       fun `element loader does cancel on video mime type`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "video/mkv", sizeOkay), `is`(true))
+       }
+
+       @Test
+       fun `element loader does cancel on text mime type`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "text/plain", sizeOkay), `is`(true))
+       }
+
+       @Test
+       fun `element loader does not cancel on text html mime type`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               assertThat(callback.value.shouldCancel(freenetURI, "text/html", sizeOkay), `is`(false))
+       }
+
+       @Test
+       fun `image loader can load image`() {
+               elementLoader.loadElement(decomposedKey)
+               verify(freenetInterface).startFetch(eq(FreenetURI(decomposedKey)), callback.capture())
+               callback.value.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png"))
+               val linkedElement = elementLoader.loadElement(decomposedKey)
+               assertThat(linkedElement, `is`(LinkedElement(normalizedKey, properties = mapOf(
+                               "type" to "image", "size" to 2451, "sizeHuman" to "2 KiB"
+               ))))
+       }
+
+       @Test
+       fun `element loader can extract description from description header`() {
+           elementLoader.loadElement(textKey)
+               verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
+               callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html"))
+               val linkedElement = elementLoader.loadElement(textKey)
+               assertThat(linkedElement, equalTo(LinkedElement(textKey, properties = mapOf(
+                               "type" to "html",
+                               "size" to 266,
+                               "sizeHuman" to "266 B",
+                               "title" to "Some Nice Page Title",
+                               "description" to "This is an example of a very nice freesite."
+               ))))
+       }
+
+       @Test
+       fun `element loader can extract description from first non-heading paragraph`() {
+           elementLoader.loadElement(textKey)
+               verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
+               callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader2.html"))
+               val linkedElement = elementLoader.loadElement(textKey)
+               assertThat(linkedElement, equalTo(LinkedElement(textKey, properties = mapOf(
+                               "type" to "html",
+                               "size" to 185,
+                               "sizeHuman" to "185 B",
+                               "title" to "Some Nice Page Title",
+                               "description" to "This is the first paragraph of the very nice freesite."
+               ))))
+       }
+
+       @Test
+       fun `element loader can not extract description if html is more complicated`() {
+           elementLoader.loadElement(textKey)
+               verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
+               callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader3.html"))
+               val linkedElement = elementLoader.loadElement(textKey)
+               assertThat(linkedElement, equalTo(LinkedElement(textKey, properties = mapOf(
+                               "type" to "html",
+                               "size" to 204,
+                               "sizeHuman" to "204 B",
+                               "title" to "Some Nice Page Title",
+                               "description" to null
+               ))))
+       }
+
+       @Test
+       fun `element loader can not extract title if it is missing`() {
+           elementLoader.loadElement(textKey)
+               verify(freenetInterface).startFetch(eq(FreenetURI(textKey)), callback.capture())
+               callback.value.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader4.html"))
+               val linkedElement = elementLoader.loadElement(textKey)
+               assertThat(linkedElement, equalTo(LinkedElement(textKey, properties = mapOf(
+                               "type" to "html",
+                               "size" to 229,
+                               "sizeHuman" to "229 B",
+                               "title" to null,
+                               "description" to "This is an example of a very nice freesite."
+               ))))
+       }
+
+       @Test
+       fun `image is not loaded again after it failed`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               callback.value.failed(freenetURI)
+               assertThat(elementLoader.loadElement(IMAGE_ID).failed, `is`(true))
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+       }
+
+       @Test
+       fun `image is loaded again after failure cache is expired`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(freenetURI), callback.capture())
+               callback.value.failed(freenetURI)
+               `when`(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(31))
+               val linkedElement = elementLoader.loadElement(IMAGE_ID)
+               assertThat(linkedElement.failed, `is`(false))
+               assertThat(linkedElement.loading, `is`(true))
+               verify(freenetInterface, times(2)).startFetch(eq(freenetURI), callback.capture())
+       }
+
+       private fun read(resource: String): ByteArray =
+                       javaClass.getResourceAsStream(resource)?.use { input ->
+                               ByteArrayOutputStream().use {
+                                       ByteStreams.copy(input, it)
+                                       it
+                               }.toByteArray()
+                       } ?: ByteArray(0)
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt
new file mode 100644 (file)
index 0000000..dbb5bcc
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.core
+
+import com.google.inject.Guice.createInjector
+import net.pterodactylus.sone.test.bindMock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.notNullValue
+import org.junit.Test
+
+/**
+ * Unit test for [ElementLoader].
+ */
+class ElementLoaderTest {
+
+       @Test
+       fun `default image loader can be loaded by guice`() {
+               val injector = createInjector(bindMock<FreenetInterface>())
+               assertThat(injector.getInstance(ElementLoader::class.java), notNullValue());
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/CreatePostCommandTest.kt
new file mode 100644 (file)
index 0000000..5fc84f0
--- /dev/null
@@ -0,0 +1,100 @@
+package net.pterodactylus.sone.fcp
+
+import com.google.common.base.Optional
+import com.google.common.base.Optional.absent
+import com.google.common.base.Optional.of
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Post
+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.notNullValue
+import org.junit.Test
+
+/**
+ * Unit test for [CreatePostCommand].
+ */
+class CreatePostCommandTest : SoneCommandTest() {
+
+       override fun createCommand(core: Core) = CreatePostCommand(core)
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with empty Sone parameter results in fcp exception`() {
+           requestWithEmptySoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid Sone parameter results in fcp exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid remote Sone parameter results in fcp exception`() {
+               requestWithValidRemoteSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request without text results in fcp exception`() {
+               parameters += "Sone" to "LocalSoneId"
+               whenever(core.getSone("LocalSoneId")).thenReturn(Optional.of(localSone))
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `request with text creates post`() {
+               parameters += "Sone" to "LocalSoneId"
+               parameters += "Text" to "Test"
+               whenever(core.getSone("LocalSoneId")).thenReturn(of(localSone))
+               val post = mock<Post>().apply { whenever(id).thenReturn("PostId") }
+               whenever(core.createPost(localSone, absent(), "Test")).thenReturn(post)
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters.get("Message"), equalTo("PostCreated"))
+               assertThat(response.replyParameters.get("Post"), equalTo("PostId"))
+       }
+
+       @Test
+       fun `request with invalid recipient results in fcp exception`() {
+               parameters += "Sone" to "LocalSoneId"
+               parameters += "Text" to "Test"
+               parameters += "Recipient" to "InvalidSoneId"
+               whenever(core.getSone("LocalSoneId")).thenReturn(of(localSone))
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `request with recipient the same as the sender returns an error response`() {
+               parameters += "Sone" to "LocalSoneId"
+               parameters += "Text" to "Test"
+               parameters += "Recipient" to "LocalSoneId"
+               whenever(core.getSone("LocalSoneId")).thenReturn(of(localSone))
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("Error"))
+               assertThat(response.replyParameters["ErrorMessage"], notNullValue())
+       }
+
+       @Test
+       fun `request with text and recipient creates post`() {
+               parameters += "Sone" to "LocalSoneId"
+               parameters += "Text" to "Test"
+               parameters += "Recipient" to "RemoteSoneId"
+               whenever(core.getSone("LocalSoneId")).thenReturn(of(localSone))
+               whenever(core.getSone("RemoteSoneId")).thenReturn(of(remoteSone))
+               val post = mock<Post>().apply { whenever(id).thenReturn("PostId") }
+               whenever(core.createPost(localSone, of(remoteSone), "Test")).thenReturn(post)
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters.get("Message"), equalTo("PostCreated"))
+               assertThat(response.replyParameters.get("Post"), equalTo("PostId"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/CreateReplyCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/CreateReplyCommandTest.kt
new file mode 100644 (file)
index 0000000..f464de4
--- /dev/null
@@ -0,0 +1,89 @@
+package net.pterodactylus.sone.fcp
+
+import com.google.common.base.Optional.of
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [CreateReplyCommand].
+ */
+class CreateReplyCommandTest : SoneCommandTest() {
+
+       private val post = mock<Post>()
+
+       override fun createCommand(core: Core) = CreateReplyCommand(core)
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with empty Sone parameter results in fcp exception`() {
+               requestWithEmptySoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid Sone parameter results in fcp exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid remote Sone parameter results in fcp exception`() {
+               requestWithValidRemoteSoneParameterResultsInFcpException()
+       }
+
+       private fun addValidLocalSoneParameter() {
+               parameters += "Sone" to "LocalSoneId"
+               whenever(core.getSone("LocalSoneId")).thenReturn(of(localSone))
+       }
+
+       @Test
+       fun `request without post parameter results in fcp exception`() {
+               addValidLocalSoneParameter()
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `request with invalid post parameter results in fcp exception`() {
+               addValidLocalSoneParameter()
+               parameters += "Post" to "InvalidPostId"
+               executeCommandAndExpectFcpException()
+       }
+
+       private fun addValidPostParameter() {
+               parameters += "Post" to "ValidPostId"
+               whenever(core.getPost("ValidPostId")).thenReturn(of(post))
+       }
+
+       @Test
+       fun `request without text results in fcp exception`() {
+               addValidLocalSoneParameter()
+               addValidPostParameter()
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `complete request creates reply`() {
+               addValidLocalSoneParameter()
+               addValidPostParameter()
+               parameters += "Text" to "Test"
+               val postReply = mock<PostReply>().apply { whenever(id).thenReturn("ReplyId") }
+               whenever(core.createReply(localSone, post, "Test")).thenReturn(postReply)
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("ReplyCreated"))
+               assertThat(response.replyParameters["Reply"], equalTo("ReplyId"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/DeletePostCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/DeletePostCommandTest.kt
new file mode 100644 (file)
index 0000000..751ad24
--- /dev/null
@@ -0,0 +1,56 @@
+package net.pterodactylus.sone.fcp
+
+import com.google.common.base.Optional.of
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.mock
+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 [DeletePostCommand].
+ */
+class DeletePostCommandTest : SoneCommandTest() {
+
+       private val postFromRemoteSone = mock<Post>().apply { whenever(sone).thenReturn(remoteSone) }
+       private val postFromLocalSone = mock<Post>().apply { whenever(sone).thenReturn(localSone) }
+       override fun createCommand(core: Core) = DeletePostCommand(core)
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameter results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid post parameter results in fcp exception`() {
+               parameters += "Post" to "InvalidPostId"
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `request with post from remote sone returns error response`() {
+               parameters += "Post" to "RemotePostId"
+               whenever(core.getPost("RemotePostId")).thenReturn(of(postFromRemoteSone))
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("Error"))
+               assertThat(response.replyParameters["ErrorCode"], equalTo("401"))
+       }
+
+       @Test
+       fun `request with post from local sone deletes posts`() {
+               parameters += "Post" to "LocalPostId"
+               whenever(core.getPost("LocalPostId")).thenReturn(of(postFromLocalSone))
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("PostDeleted"))
+               verify(core).deletePost(postFromLocalSone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/DeleteReplyCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/DeleteReplyCommandTest.kt
new file mode 100644 (file)
index 0000000..013b1c1
--- /dev/null
@@ -0,0 +1,57 @@
+package net.pterodactylus.sone.fcp
+
+import com.google.common.base.Optional.of
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.test.mock
+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 [DeleteReplyCommand].
+ */
+class DeleteReplyCommandTest : SoneCommandTest() {
+
+       private val remotePostReply = mock<PostReply>().apply { whenever(sone).thenReturn(remoteSone) }
+       private val localPostReply = mock<PostReply>().apply { whenever(sone).thenReturn(localSone) }
+
+       override fun createCommand(core: Core) = DeleteReplyCommand(core)
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid post reply parameter results in fcp exception`() {
+               parameters += "Reply" to "InvalidReplyId"
+               executeCommandAndExpectFcpException()
+       }
+
+       @Test
+       fun `request with remote post reply parameter results in error response`() {
+           parameters += "Reply" to "RemoteReplyId"
+               whenever(core.getPostReply("RemoteReplyId")).thenReturn(of(remotePostReply))
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("Error"))
+               assertThat(response.replyParameters["ErrorCode"], equalTo("401"))
+       }
+
+       @Test
+       fun `request with local post reply parameter deletes reply`() {
+           parameters += "Reply" to "RemoteReplyId"
+               whenever(core.getPostReply("RemoteReplyId")).thenReturn(of(localPostReply))
+               val response = command.execute(parameters)
+               assertThat(response.replyParameters["Message"], equalTo("ReplyDeleted"))
+               verify(core).deleteReply(localPostReply)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/FcpInterfaceTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/FcpInterfaceTest.kt
new file mode 100644 (file)
index 0000000..6e1a06c
--- /dev/null
@@ -0,0 +1,250 @@
+@file:Suppress("DEPRECATION")
+
+package net.pterodactylus.sone.fcp
+
+import com.google.inject.Guice
+import freenet.pluginmanager.PluginNotFoundException
+import freenet.pluginmanager.PluginReplySender
+import freenet.support.SimpleFieldSet
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.fcp.FcpInterface.AccessAuthorizer
+import net.pterodactylus.sone.fcp.FcpInterface.CommandSupplier
+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.fcp.event.FcpInterfaceActivatedEvent
+import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent
+import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged
+import net.pterodactylus.sone.freenet.fcp.Command.AccessType
+import net.pterodactylus.sone.freenet.fcp.Command.AccessType.FULL_FCP
+import net.pterodactylus.sone.freenet.fcp.Command.AccessType.RESTRICTED_FCP
+import net.pterodactylus.sone.freenet.fcp.Command.Response
+import net.pterodactylus.sone.test.capture
+import net.pterodactylus.sone.test.isProvidedBy
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [FcpInterface] and its subclasses.
+ */
+class FcpInterfaceTest {
+
+       private val core = mock<Core>()
+       private val workingCommand = mock<AbstractSoneCommand>().apply {
+               whenever(execute(any())).thenReturn(Response("Working", SimpleFieldSet(true).apply {
+                       putSingle("ReallyWorking", "true")
+               }))
+       }
+       private val brokenCommand = mock<AbstractSoneCommand>().apply {
+               whenever(execute(any())).thenThrow(RuntimeException::class.java)
+       }
+       private val commandSupplier = object : CommandSupplier() {
+               override fun supplyCommands(core: Core): Map<String, AbstractSoneCommand> {
+                       return mapOf(
+                                       "Working" to workingCommand,
+                                       "Broken" to brokenCommand
+                       )
+               }
+       }
+       private val accessAuthorizer = mock<AccessAuthorizer>()
+       private val fcpInterface = FcpInterface(core, commandSupplier, accessAuthorizer)
+       private val pluginReplySender = mock<PluginReplySender>()
+       private val parameters = SimpleFieldSet(true)
+       private val replyParameters = capture<SimpleFieldSet>()
+
+       @Test
+       fun `fcp interface is instantiated as singleton`() {
+               val injector = Guice.createInjector(Core::class.isProvidedBy(core))
+               assertThat(injector.getInstance(FcpInterface::class.java), sameInstance(injector.getInstance(FcpInterface::class.java)))
+       }
+
+       @Test
+       fun `fcp interface can be activated`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               assertThat(fcpInterface.isActive, equalTo(true))
+       }
+
+       @Test
+       fun `fcp interface can be deactivated`() {
+               fcpInterface.fcpInterfaceDeactivated(FcpInterfaceDeactivatedEvent())
+               assertThat(fcpInterface.isActive, equalTo(false))
+       }
+
+       private fun setAndVerifyAccessRequired(fullAccessRequired: FullAccessRequired) {
+               fcpInterface.fullAccessRequiredChanged(FullAccessRequiredChanged(fullAccessRequired))
+               assertThat(fcpInterface.fullAccessRequired, equalTo(fullAccessRequired))
+       }
+
+       @Test
+       fun `set full access required can set access to no`() {
+               setAndVerifyAccessRequired(NO)
+       }
+
+       @Test
+       fun `set full access required can set access to writing`() {
+               setAndVerifyAccessRequired(WRITING)
+       }
+
+       @Test
+       fun `set full access required can set access to always`() {
+               setAndVerifyAccessRequired(ALWAYS)
+       }
+
+       @Test
+       fun `sending command to inactive fcp interface results in 503 error reply`() {
+               fcpInterface.fcpInterfaceDeactivated(FcpInterfaceDeactivatedEvent())
+               parameters.putSingle("Identifier", "Test")
+               fcpInterface.handle(pluginReplySender, parameters, null, 0)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("503"))
+       }
+
+       @Test
+       fun `exception while sending reply does not result in exception`() {
+               fcpInterface.fcpInterfaceDeactivated(FcpInterfaceDeactivatedEvent())
+               whenever(pluginReplySender.send(ArgumentMatchers.any())).thenThrow(PluginNotFoundException::class.java)
+               fcpInterface.handle(pluginReplySender, parameters, null, 0)
+       }
+
+       @Test
+       fun `sending command over non-authorized connection results in 401 error reply`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               parameters.putSingle("Identifier", "Test")
+               parameters.putSingle("Message", "Working")
+               fcpInterface.handle(pluginReplySender, parameters, null, RESTRICTED_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("401"))
+       }
+
+       @Test
+       fun `sending unknown command results in 404 error reply`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               parameters.putSingle("Identifier", "Test")
+               fcpInterface.handle(pluginReplySender, parameters, null, RESTRICTED_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("404"))
+       }
+
+       @Test
+       fun `sending working command without identifier results in 400 error code`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               whenever(accessAuthorizer.authorized(any(), any(), anyBoolean())).thenReturn(true)
+               parameters.putSingle("Message", "Working")
+               fcpInterface.handle(pluginReplySender, parameters, null, FULL_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("400"))
+       }
+
+       @Test
+       fun `sending working command with empty identifier results in 400 error code`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               whenever(accessAuthorizer.authorized(any(), any(), anyBoolean())).thenReturn(true)
+               parameters.putSingle("Message", "Working")
+               parameters.putSingle("Identifier", "")
+               fcpInterface.handle(pluginReplySender, parameters, null, FULL_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("400"))
+       }
+
+       @Test
+       fun `sending working command with identifier results in working reply`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               whenever(accessAuthorizer.authorized(any(), any(), anyBoolean())).thenReturn(true)
+               parameters.putSingle("Message", "Working")
+               parameters.putSingle("Identifier", "Test")
+               fcpInterface.handle(pluginReplySender, parameters, null, FULL_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Working"))
+               assertThat(replyParameters.value["ReallyWorking"], equalTo("true"))
+       }
+
+       @Test
+       fun `sending broken  command with identifier results in 500 error reply`() {
+               fcpInterface.fcpInterfaceActivated(FcpInterfaceActivatedEvent())
+               whenever(accessAuthorizer.authorized(any(), any(), anyBoolean())).thenReturn(true)
+               parameters.putSingle("Message", "Broken")
+               parameters.putSingle("Identifier", "Test")
+               fcpInterface.handle(pluginReplySender, parameters, null, FULL_FCP.ordinal)
+               verify(pluginReplySender).send(replyParameters.capture())
+               assertThat(replyParameters.value["Message"], equalTo("Error"))
+               assertThat(replyParameters.value["ErrorCode"], equalTo("500"))
+       }
+
+}
+
+class CommandSupplierTest {
+
+       private val core = mock<Core>()
+       private val commandSupplier = CommandSupplier()
+
+       @Test
+       fun `command supplier supplies all commands`() {
+               val commands = commandSupplier.supplyCommands(core)
+               assertThat(commands.keys, containsInAnyOrder(
+                               "CreatePost",
+                               "CreateReply",
+                               "DeletePost",
+                               "DeleteReply",
+                               "GetLocalSones",
+                               "GetPost",
+                               "GetPostFeed",
+                               "GetPosts",
+                               "GetSone",
+                               "GetSones",
+                               "LikePost",
+                               "LikeReply",
+                               "LockSone",
+                               "UnlockSone",
+                               "Version"
+               ))
+       }
+
+       @Test
+       fun `command supplier is instantiated as singleton`() {
+               val injector = Guice.createInjector()
+               assertThat(injector.getInstance(CommandSupplier::class.java), sameInstance(injector.getInstance(CommandSupplier::class.java)))
+       }
+
+}
+
+class AccessAuthorizerTest {
+
+       private val accessAuthorizer = AccessAuthorizer()
+
+       @Test
+       fun `access authorizer is instantiated as singleton`() {
+               val injector = Guice.createInjector()
+               assertThat(injector.getInstance(AccessAuthorizer::class.java), sameInstance(injector.getInstance(AccessAuthorizer::class.java)))
+       }
+
+       @Test
+       fun `access authorizer makes correct decisions`() {
+               AccessType.values().forEach { accessType ->
+                       FullAccessRequired.values().forEach { fullAccessRequired ->
+                               listOf(false, true).forEach { commandRequiresWriteAccess ->
+                                       assertThat("$accessType, $fullAccessRequired, $commandRequiresWriteAccess", accessAuthorizer.authorized(accessType, fullAccessRequired, commandRequiresWriteAccess), equalTo(
+                                                       accessType != RESTRICTED_FCP ||
+                                                                       fullAccessRequired == NO ||
+                                                                       (fullAccessRequired == WRITING && !commandRequiresWriteAccess)
+                                       ))
+                               }
+                       }
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetLocalSonesCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetLocalSonesCommandTest.kt
new file mode 100644 (file)
index 0000000..2a78efc
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetLocalSonesCommand].
+ */
+class GetLocalSonesCommandTest : SoneCommandTest() {
+
+       private val sone1 = createSone("Id1", "Name1", "First1", "Last1", 1000L)
+       private val sone2 = createSone("Id2", "Name2", "First2", "Last2", 2000L)
+
+       override fun createCommand(core: Core) = GetLocalSonesCommand(core)
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `command returns local sones`() {
+               val localSones = setOf(sone1, sone2)
+               whenever(core.localSones).thenReturn(localSones)
+               val response = command.execute(null)
+               val replyParameters = response.replyParameters
+               assertThat(replyParameters["Message"], equalTo("ListLocalSones"))
+               assertThat(replyParameters["LocalSones.Count"], equalTo("2"))
+               assertThat(replyParameters.parseSone("LocalSones.0."), matchesSone(sone1))
+               assertThat(replyParameters.parseSone("LocalSones.1."), matchesSone(sone2))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostCommandTest.kt
new file mode 100644 (file)
index 0000000..b7f911c
--- /dev/null
@@ -0,0 +1,109 @@
+package net.pterodactylus.sone.fcp
+
+import freenet.support.SimpleFieldSet
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [GetPostCommand].
+ */
+class GetPostCommandTest : SoneCommandTest() {
+
+       private val sone = mock<Sone>().apply {
+               whenever(id).thenReturn("SoneId")
+       }
+       private val post = createPost("ValidPostId", sone, null, 1000, "Post Text\r\nSecond \\Line")
+       private val sone1 = mock<Sone>().apply { whenever(id).thenReturn("Sone1") }
+       private val sone2 = mock<Sone>().apply { whenever(id).thenReturn("Sone2") }
+       private val postReply1 = createReply("ReplyId1", sone1, post, 1000, "Reply 1")
+       private val postReply2 = createReply("ReplyId2", sone2, post, 2000, "Reply 2")
+
+       override fun createCommand(core: Core) = GetPostCommand(core)
+
+       @Before
+       fun setupPostWithLikesAndReplies() {
+               whenever(core.getPost("ValidPostId")).thenReturn(post.asOptional())
+               whenever(core.getLikes(post)).thenReturn(setOf(sone1, sone2))
+               val replies = listOf(postReply1, postReply2)
+               whenever(core.getReplies("ValidPostId")).thenReturn(replies)
+       }
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `request without any parameter results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid post parameter results in fcp exception`() {
+               parameters += "Post" to "InvalidPostId"
+               executeCommandAndExpectFcpException()
+       }
+
+       private fun verifyPostWithLikes(replyParameters: SimpleFieldSet) {
+               assertThat(replyParameters["Message"], equalTo("Post"))
+               assertThat(replyParameters.parsePost("Post."), matchesPost(post))
+               assertThat(replyParameters["Post.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Post.Likes.$it.ID"] }, containsInAnyOrder("Sone1", "Sone2"))
+       }
+
+       private fun verifyReplies(replyParameters: SimpleFieldSet) {
+               assertThat(replyParameters["Post.Replies.Count"], equalTo("2"))
+               assertThat(replyParameters.parsePost("Post.Replies.0."), matchesReply(postReply1))
+               assertThat(replyParameters.parsePost("Post.Replies.1."), matchesReply(postReply2))
+       }
+
+       @Test
+       fun `request with valid post parameter returns post response`() {
+               parameters += "Post" to "ValidPostId"
+               val replyParameters = command.execute(parameters).replyParameters
+               verifyPostWithLikes(replyParameters)
+               assertThat(replyParameters["Post.Recipient"], nullValue())
+               verifyReplies(replyParameters)
+       }
+
+       @Test
+       fun `request with valid post parameter without replies returns post response without replies`() {
+               parameters += "Post" to "ValidPostId"
+               parameters += "IncludeReplies" to "false"
+               val replyParameters = command.execute(parameters).replyParameters
+               verifyPostWithLikes(replyParameters)
+               assertThat(replyParameters["Post.Recipient"], nullValue())
+               assertThat(replyParameters["Post.Replies.Count"], nullValue())
+       }
+
+       @Test
+       fun `request with valid post parameter returns post response with recipient`() {
+               parameters += "Post" to "ValidPostId"
+               whenever(post.recipientId).thenReturn("Sone2".asOptional())
+               val replyParameters = command.execute(parameters).replyParameters
+               verifyPostWithLikes(replyParameters)
+               assertThat(replyParameters["Post.Recipient"], equalTo("Sone2"))
+               verifyReplies(replyParameters)
+       }
+
+       @Test
+       fun `request with valid post parameter without replies returns post response without replies but with recipient`() {
+               parameters += "Post" to "ValidPostId"
+               parameters += "IncludeReplies" to "false"
+               whenever(post.recipientId).thenReturn("Sone2".asOptional())
+               val replyParameters = command.execute(parameters).replyParameters
+               verifyPostWithLikes(replyParameters)
+               assertThat(replyParameters["Post.Recipient"], equalTo("Sone2"))
+               assertThat(replyParameters["Post.Replies.Count"], nullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostFeedCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostFeedCommandTest.kt
new file mode 100644 (file)
index 0000000..72daa07
--- /dev/null
@@ -0,0 +1,179 @@
+package net.pterodactylus.sone.fcp
+
+import freenet.support.SimpleFieldSet
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetPostFeedCommand].
+ */
+class GetPostFeedCommandTest : SoneCommandTest() {
+
+       private val sone1 = createSone("Sone1", "Sone 1", "Sone", "#1", 1000)
+       private val sone2 = createSone("Sone2", "Sone 2", "Sone", "#2", 2000)
+       private val sone3 = createSone("Sone3", "Sone 3", "Sone", "#3", 3000)
+       private val sone4 = createSone("Sone4", "Sone 4", "Sone", "#4", 4000)
+       private val friend1 = createSone("Friend1", "Friend 1", "Friend", "#1", 5000)
+       private val post1 = createPost("Post1", sone1, null, 1000, "Post 1")
+       private val post1Reply1 = createReply("Post1Reply1", sone1, post1, 10000, "Post 1, Reply 1")
+       private val post1Reply2 = createReply("Post1Reply2", sone2, post1, 20000, "Post 1, Reply 2")
+       private val post2 = createPost("Post2", sone2, "Recipient 2", 2000, "Post 2")
+       private val post2Reply1 = createReply("Post2Reply1", sone3, post2, 30000, "Post 2, Reply 1")
+       private val post2Reply2 = createReply("Post2Reply2", sone4, post2, 40000, "Post 2, Reply 2")
+       private val friendPost1 = createPost("FriendPost1", friend1, null, 1500, "Friend Post 1")
+       private val directedPost = createPost("DirectedPost1", sone3, "ValidSoneId", 500, "Hey!")
+
+       override fun createCommand(core: Core) = GetPostFeedCommand(core)
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with empty Sone parameter results in fcp exception`() {
+               requestWithEmptySoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid Sone parameter results in fcp exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid remote Sone parameter results in fcp exception`() {
+               requestWithValidRemoteSoneParameterResultsInFcpException()
+       }
+
+       private fun setupAllPostsAndReplies() {
+               parameters += "Sone" to "ValidSoneId"
+               whenever(localSone.id).thenReturn("ValidSoneId")
+               whenever(core.getSone("ValidSoneId")).thenReturn(localSone.asOptional())
+               whenever(core.getSone("Friend1")).thenReturn(friend1.asOptional())
+               whenever(core.getLikes(post1)).thenReturn(setOf(sone3, sone4))
+               whenever(core.getLikes(post1Reply1)).thenReturn(setOf(sone2, sone3))
+               whenever(core.getLikes(post1Reply2)).thenReturn(setOf(sone3))
+               whenever(core.getReplies("Post1")).thenReturn(listOf(post1Reply1, post1Reply2))
+               whenever(core.getLikes(post2)).thenReturn(setOf(sone1, sone2))
+               whenever(core.getLikes(post2Reply1)).thenReturn(setOf(sone4, sone1))
+               whenever(core.getLikes(post2Reply2)).thenReturn(setOf(sone1, sone2, sone3))
+               whenever(core.getReplies("Post2")).thenReturn(listOf(post2Reply1, post2Reply2))
+               whenever(localSone.posts).thenReturn(listOf(post2, post1))
+               whenever(core.getLikes(friendPost1)).thenReturn(setOf(sone1, friend1))
+               whenever(friend1.posts).thenReturn(listOf(friendPost1))
+               whenever(localSone.friends).thenReturn(setOf("Friend1", "Friend2"))
+               whenever(core.getDirectedPosts("ValidSoneId")).thenReturn(setOf(directedPost))
+               whenever(core.getLikes(directedPost)).thenReturn(setOf(sone2, sone4))
+       }
+
+       private fun verifyFirstPost(replyParameters: SimpleFieldSet) {
+               assertThat(replyParameters.parsePost("Posts.0."), matchesPost(post2))
+               assertThat(replyParameters["Posts.0.Replies.Count"], equalTo("2"))
+               assertThat(replyParameters.parseReply("Posts.0.Replies.0."), matchesReply(post2Reply1))
+               assertThat(replyParameters["Posts.0.Replies.0.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.0.Replies.0.Likes.$it.ID"] }, containsInAnyOrder("Sone1", "Sone4"))
+               assertThat(replyParameters.parseReply("Posts.0.Replies.1."), matchesReply(post2Reply2))
+               assertThat(replyParameters["Posts.0.Replies.1.Likes.Count"], equalTo("3"))
+               assertThat((0..2).map { replyParameters["Posts.0.Replies.1.Likes.$it.ID"] }, containsInAnyOrder("Sone1", "Sone2", "Sone3"))
+               assertThat(replyParameters["Posts.0.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.0.Likes.$it.ID"] }, containsInAnyOrder("Sone1", "Sone2"))
+       }
+
+       private fun verifySecondPost(replyParameters: SimpleFieldSet, index: Int = 1) {
+               assertThat(replyParameters.parsePost("Posts.$index."), matchesPost(friendPost1))
+               assertThat(replyParameters["Posts.$index.Replies.Count"], equalTo("0"))
+               assertThat(replyParameters["Posts.$index.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Likes.$it.ID"] }, containsInAnyOrder("Sone1", "Friend1"))
+       }
+
+       private fun verifyThirdPost(replyParameters: SimpleFieldSet, index: Int = 2) {
+               assertThat(replyParameters.parsePost("Posts.$index."), matchesPost(post1))
+               assertThat(replyParameters.parseReply("Posts.$index.Replies.0."), matchesReply(post1Reply1))
+               assertThat(replyParameters.parseReply("Posts.$index.Replies.1."), matchesReply(post1Reply2))
+               assertThat(replyParameters["Posts.$index.Replies.0.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Replies.0.Likes.$it.ID"] }, containsInAnyOrder("Sone2", "Sone3"))
+               assertThat(replyParameters["Posts.$index.Replies.1.Likes.Count"], equalTo("1"))
+               assertThat(replyParameters["Posts.$index.Replies.1.Likes.0.ID"], equalTo("Sone3"))
+               assertThat(replyParameters["Posts.$index.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Likes.$it.ID"] }, containsInAnyOrder("Sone3", "Sone4"))
+       }
+
+       private fun verifyFourthPost(replyParameters: SimpleFieldSet, index: Int = 3) {
+               assertThat(replyParameters.parsePost("Posts.$index."), matchesPost(directedPost))
+               assertThat(replyParameters["Posts.$index.Replies.Count"], equalTo("0"))
+               assertThat(replyParameters["Posts.$index.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Likes.$it.ID"] }, containsInAnyOrder("Sone2", "Sone4"))
+       }
+
+       @Test
+       fun `request with valid local Sone parameter results in the post feed with all required posts`() {
+               setupAllPostsAndReplies()
+
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("PostFeed"))
+               assertThat(replyParameters["Posts.Count"], equalTo("4"))
+
+               verifyFirstPost(replyParameters)
+               verifySecondPost(replyParameters)
+               verifyThirdPost(replyParameters)
+               verifyFourthPost(replyParameters)
+       }
+
+       @Test
+       fun `request with larger start than number of posts returns empty feed`() {
+               setupAllPostsAndReplies()
+               parameters += "StartPost" to "20"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("PostFeed"))
+               assertThat(replyParameters["Posts.Count"], equalTo("0"))
+       }
+
+       @Test
+       fun `request with max posts of 2 returns the first two posts`() {
+               setupAllPostsAndReplies()
+               parameters += "MaxPosts" to "2"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("PostFeed"))
+               assertThat(replyParameters["Posts.Count"], equalTo("2"))
+
+               verifyFirstPost(replyParameters)
+               verifySecondPost(replyParameters)
+       }
+
+       @Test
+       fun `request with max posts of 2 and start post of 1 returns the center two posts`() {
+               setupAllPostsAndReplies()
+               parameters += "StartPost" to "1"
+               parameters += "MaxPosts" to "2"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("PostFeed"))
+               assertThat(replyParameters["Posts.Count"], equalTo("2"))
+
+               verifySecondPost(replyParameters, 0)
+               verifyThirdPost(replyParameters, 1)
+       }
+
+       @Test
+       fun `request with max posts of 2 and start post of 3 returns the last post`() {
+               setupAllPostsAndReplies()
+               parameters += "StartPost" to "3"
+               parameters += "MaxPosts" to "2"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("PostFeed"))
+               assertThat(replyParameters["Posts.Count"], equalTo("1"))
+
+               verifyFourthPost(replyParameters, 0)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostsCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetPostsCommandTest.kt
new file mode 100644 (file)
index 0000000..6483085
--- /dev/null
@@ -0,0 +1,117 @@
+package net.pterodactylus.sone.fcp
+
+import freenet.support.SimpleFieldSet
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetPostsCommand].
+ */
+class GetPostsCommandTest : SoneCommandTest() {
+
+       private val sone1 = createSone("Sone1", "Sone1", "Sone", "#1", 1000)
+       private val sone2 = createSone("Sone2", "Sone2", "Sone", "#2", 2000)
+       private val post1 = createPost("Post1", remoteSone, null, 1000, "Post \\1\n")
+       private val post2 = createPost("Post2", localSone, null, 2000, "Post \\2\r")
+       private val post2Reply1 = createReply("Post2Reply1", localSone, post2, 2000, "Reply 1")
+       private val post2Reply2 = createReply("Post2Reply2", remoteSone, post2, 3000, "Reply 2")
+
+       override fun createCommand(core: Core) = GetPostsCommand(core)
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with empty Sone parameter results in fcp exception`() {
+               requestWithEmptySoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid Sone parameter results in fcp exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       private fun setupPostsAndReplies() {
+               whenever(core.getLikes(post1)).thenReturn(setOf(localSone, sone1))
+               whenever(core.getLikes(post2Reply1)).thenReturn(setOf(remoteSone, sone2))
+               whenever(core.getReplies("Post2")).thenReturn(listOf(post2Reply1, post2Reply2))
+               whenever(localSone.id).thenReturn("LocalSone")
+               whenever(remoteSone.id).thenReturn("RemoteSone")
+               whenever(core.getSone("LocalSone")).thenReturn(localSone.asOptional())
+               whenever(core.getSone("ValidSoneId")).thenReturn(remoteSone.asOptional())
+               whenever(remoteSone.posts).thenReturn(listOf(post2, post1))
+               parameters += "Sone" to "ValidSoneId"
+       }
+
+       private fun verifyFirstPost(replyParameters: SimpleFieldSet, index: Int = 0) {
+               assertThat(replyParameters.parsePost("Posts.$index."), matchesPost(post2))
+               assertThat(replyParameters["Posts.$index.Replies.Count"], equalTo("2"))
+               assertThat(replyParameters.parseReply("Posts.$index.Replies.0."), matchesReply(post2Reply1))
+               assertThat(replyParameters["Posts.$index.Replies.0.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Replies.0.Likes.$it.ID"] }, containsInAnyOrder("RemoteSone", "Sone2"))
+               assertThat(replyParameters.parseReply("Posts.$index.Replies.1."), matchesReply(post2Reply2))
+       }
+
+       private fun verifySecondPost(replyParameters: SimpleFieldSet, index: Int = 1) {
+               assertThat(replyParameters.parsePost("Posts.$index."), matchesPost(post1))
+               assertThat(replyParameters["Posts.$index.Likes.Count"], equalTo("2"))
+               assertThat((0..1).map { replyParameters["Posts.$index.Likes.$it.ID"] }, containsInAnyOrder("LocalSone", "Sone1"))
+       }
+
+       @Test
+       fun `request with valid sone parameter lists all posts of the sone`() {
+               setupPostsAndReplies()
+
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("Posts"))
+               assertThat(replyParameters["Posts.Count"], equalTo("2"))
+               verifyFirstPost(replyParameters)
+               verifySecondPost(replyParameters)
+       }
+
+       @Test
+       fun `request with a maximum of 1 post returns only 1 post`() {
+               setupPostsAndReplies()
+               parameters += "MaxPosts" to "1"
+
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Posts"))
+               assertThat(replyParameters["Posts.Count"], equalTo("1"))
+               verifyFirstPost(replyParameters)
+       }
+
+       @Test
+       fun `request starting at the second post returns only 1 post`() {
+               setupPostsAndReplies()
+               parameters += "StartPost" to "1"
+
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Posts"))
+               assertThat(replyParameters["Posts.Count"], equalTo("1"))
+               verifySecondPost(replyParameters, 0)
+       }
+
+       @Test
+       fun `request skipping more posts than exist returns an empty list`() {
+               setupPostsAndReplies()
+               parameters += "StartPost" to "20"
+
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Posts"))
+               assertThat(replyParameters["Posts.Count"], equalTo("0"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetSoneCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetSoneCommandTest.kt
new file mode 100644 (file)
index 0000000..e73160b
--- /dev/null
@@ -0,0 +1,93 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.freenet.fcp.FcpException
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [GetSoneCommand].
+ */
+class GetSoneCommandTest : SoneCommandTest() {
+
+       private val sone = createSone("SoneId", "Sone", "Sone", "#1", 1000).apply {
+               profile.addField("Test").value = "true"
+               profile.addField("More Test").value = "also true"
+       }
+
+       override fun createCommand(core: Core) = GetSoneCommand(core)
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `request without any parameters results in fcp exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with empty Sone parameter results in fcp exception`() {
+               requestWithEmptySoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid Sone parameter results in fcp exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid Sone parameter results in response with Sone information`() {
+               whenever(core.getSone("SoneId")).thenReturn(sone.asOptional())
+               whenever(core.getSone(null)).thenReturn(null.asOptional())
+               parameters += "Sone" to "SoneId"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Sone"))
+               assertThat(replyParameters.parseSone("Sone."), matchesSone(sone))
+               assertThat(replyParameters["Sone.Followed"], nullValue())
+       }
+
+       @Test
+       fun `request with local sone parameter results in followed being true for friend sone`() {
+               whenever(core.getSone("SoneId")).thenReturn(sone.asOptional())
+               whenever(core.getSone("LocalSone")).thenReturn(localSone.asOptional())
+               whenever(localSone.id).thenReturn("LocalSone")
+               whenever(localSone.hasFriend("SoneId")).thenReturn(true)
+               parameters += "Sone" to "SoneId"
+               parameters += "LocalSone" to "LocalSone"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Sone"))
+               assertThat(replyParameters.parseSone("Sone."), matchesSone(sone))
+               assertThat(replyParameters["Sone.Followed"], equalTo("true"))
+       }
+       
+       @Test
+       fun `request with local sone parameter results in followed being false for non-friend sone`() {
+               whenever(core.getSone("SoneId")).thenReturn(sone.asOptional())
+               whenever(core.getSone("LocalSone")).thenReturn(localSone.asOptional())
+               whenever(localSone.id).thenReturn("LocalSone")
+               parameters += "Sone" to "SoneId"
+               parameters += "LocalSone" to "LocalSone"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Sone"))
+               assertThat(replyParameters.parseSone("Sone."), matchesSone(sone))
+               assertThat(replyParameters["Sone.Followed"], equalTo("false"))
+       }
+
+       @Test
+       fun `request with remote sone as local sone parameter results in fcp exception`() {
+               whenever(core.getSone("SoneId")).thenReturn(sone.asOptional())
+               whenever(core.getSone("RemoteSone")).thenReturn(remoteSone.asOptional())
+               whenever(localSone.id).thenReturn("RemoteSone")
+               parameters += "Sone" to "SoneId"
+               parameters += "LocalSone" to "RemoteSone"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/GetSonesCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/GetSonesCommandTest.kt
new file mode 100644 (file)
index 0000000..252eae3
--- /dev/null
@@ -0,0 +1,73 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [GetSonesCommand].
+ */
+class GetSonesCommandTest : SoneCommandTest() {
+
+       private val sone1 = createSone("SoneId1", "Sone1", "Sone", "#1", 1000)
+       private val sone2 = createSone("SoneId2", "Sone2", "Sone", "#2", 2000)
+       private val sone3 = createSone("SoneId3", "Sone3", "Sone", "#3", 3000)
+
+       override fun createCommand(core: Core) = GetSonesCommand(core)
+
+       @Before
+       fun setupSones() {
+               whenever(core.sones).thenReturn(setOf(sone2, sone3, sone1))
+       }
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `request without parameters lists all sones`() {
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("Sones"))
+               assertThat(replyParameters["Sones.Count"], equalTo("3"))
+               assertThat(replyParameters.parseSone("Sones.0."), matchesSone(sone1))
+               assertThat(replyParameters.parseSone("Sones.1."), matchesSone(sone2))
+               assertThat(replyParameters.parseSone("Sones.2."), matchesSone(sone3))
+       }
+
+       @Test
+       fun `skipping the first sone lists the last two sones`() {
+               parameters += "StartSone" to "1"
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("Sones"))
+               assertThat(replyParameters["Sones.Count"], equalTo("2"))
+               assertThat(replyParameters.parseSone("Sones.0."), matchesSone(sone2))
+               assertThat(replyParameters.parseSone("Sones.1."), matchesSone(sone3))
+       }
+
+       @Test
+       fun `requesting only two sones lists the first two sones`() {
+               parameters += "MaxSones" to "2"
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("Sones"))
+               assertThat(replyParameters["Sones.Count"], equalTo("2"))
+               assertThat(replyParameters.parseSone("Sones.0."), matchesSone(sone1))
+               assertThat(replyParameters.parseSone("Sones.1."), matchesSone(sone2))
+       }
+
+       @Test
+       fun `skipping more sones than there are lists no sones`() {
+               parameters += "StartSone" to "20"
+               val replyParameters = command.execute(parameters).replyParameters
+
+               assertThat(replyParameters["Message"], equalTo("Sones"))
+               assertThat(replyParameters["Sones.Count"], equalTo("0"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/LikePostCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/LikePostCommandTest.kt
new file mode 100644 (file)
index 0000000..54f784f
--- /dev/null
@@ -0,0 +1,82 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.freenet.fcp.FcpException
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+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 [LikePostCommand].
+ */
+class LikePostCommandTest : SoneCommandTest() {
+
+       private val post = createPost("PostId", mock<Sone>(), null, 1000, "Text")
+
+       override fun createCommand(core: Core) = LikePostCommand(core)
+
+       @Before
+       fun setupPostAndSones() {
+               whenever(core.getPost("PostId")).thenReturn(post.asOptional())
+               whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone.asOptional())
+               whenever(core.getSone("LocalSoneId")).thenReturn(localSone.asOptional())
+       }
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without parameters results in FCP exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid post id results in FCP exception`() {
+               parameters += "Post" to "InvalidPostId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with missing local sone results in FCP exception`() {
+               parameters += "Post" to "PostId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with invalid sone results in FCP exception`() {
+               parameters += "Post" to "PostId"
+               parameters += "Sone" to "InvalidSoneId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with valid remote sone results in FCP exception`() {
+               parameters += "Post" to "PostId"
+               parameters += "Sone" to "RemoteSoneId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with valid parameters adds post to liked posts for sone`() {
+               whenever(core.getLikes(post)).thenReturn(setOf(mock<Sone>(), mock<Sone>(), mock<Sone>()))
+               parameters += "Post" to "PostId"
+               parameters += "Sone" to "LocalSoneId"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("PostLiked"))
+               assertThat(replyParameters["LikeCount"], equalTo("3"))
+               verify(localSone).addLikedPostId("PostId")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/LikeReplyCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/LikeReplyCommandTest.kt
new file mode 100644 (file)
index 0000000..69ace0a
--- /dev/null
@@ -0,0 +1,83 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.freenet.fcp.FcpException
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+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 [LikeReplyCommand].
+ */
+class LikeReplyCommandTest : SoneCommandTest() {
+
+       private val reply = createReply("ReplyId", mock<Sone>(), mock<Post>(), 1000, "Text")
+
+       override fun createCommand(core: Core) = LikeReplyCommand(core)
+
+       @Before
+       fun setupRepliesAndSones() {
+               whenever(core.getPostReply("ReplyId")).thenReturn(reply.asOptional())
+               whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone.asOptional())
+               whenever(core.getSone("LocalSoneId")).thenReturn(localSone.asOptional())
+       }
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without parameters results in FCP exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid reply results in FCP exception`() {
+               parameters += "Reply" to "InvalidReplyId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request without sone results in FCP exception`() {
+               parameters += "Reply" to "ReplyId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with invalid sone results in FCP exception`() {
+               parameters += "Reply" to "ReplyId"
+               parameters += "Sone" to "InvalidSoneId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with remote sone results in FCP exception`() {
+               parameters += "Reply" to "ReplyId"
+               parameters += "Sone" to "RemoteSoneId"
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       @Test
+       fun `request with local sone adds reply id to sone`() {
+               whenever(core.getLikes(reply)).thenReturn(setOf(mock<Sone>(), mock<Sone>(), mock<Sone>()))
+               parameters += "Reply" to "ReplyId"
+               parameters += "Sone" to "LocalSoneId"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("ReplyLiked"))
+               assertThat(replyParameters["LikeCount"], equalTo("3"))
+               verify(localSone).addLikedReplyId("ReplyId")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/LockSoneCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/LockSoneCommandTest.kt
new file mode 100644 (file)
index 0000000..1caea96
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+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 [LockSoneCommand].
+ */
+class LockSoneCommandTest : SoneCommandTest() {
+
+       override fun createCommand(core: Core) = LockSoneCommand(core)
+
+       @Before
+       fun setupSones() {
+               whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone.asOptional())
+               whenever(core.getSone("LocalSoneId")).thenReturn(localSone.asOptional())
+               whenever(localSone.id).thenReturn("LocalSoneId")
+       }
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameters results in FCP exception`() {
+           requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid sone parameter results in FCP exception`() {
+           requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid remote sone parameter results in FCP exception`() {
+           requestWithValidRemoteSoneParameterResultsInFcpException()
+       }
+       
+       @Test
+       fun `request with local sone parameter locks the sone`() {
+           parameters += "Sone" to "LocalSoneId"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("SoneLocked"))
+               assertThat(replyParameters["Sone"], equalTo("LocalSoneId"))
+               verify(core).lockSone(localSone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/SoneCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/SoneCommandTest.kt
new file mode 100644 (file)
index 0000000..aa38f88
--- /dev/null
@@ -0,0 +1,135 @@
+package net.pterodactylus.sone.fcp
+
+import com.google.common.base.Optional
+import com.google.common.base.Optional.absent
+import freenet.support.SimpleFieldSet
+import net.pterodactylus.sone.core.Core
+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.fcp.FcpException
+import net.pterodactylus.sone.template.SoneAccessor
+import net.pterodactylus.sone.test.OneByOneMatcher
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.junit.Before
+import org.junit.Rule
+import org.junit.rules.ExpectedException
+import org.mockito.ArgumentMatchers.anyString
+
+/**
+ * Base class for Sone FCP command tests.
+ */
+abstract class SoneCommandTest {
+
+       @Rule @JvmField val expectedException = ExpectedException.none()!!
+
+       protected val core = mock<Core>()
+       protected val command: AbstractSoneCommand by lazy { createCommand(core) }
+
+       protected val parameters = SimpleFieldSet(true)
+       protected val localSone = mock<Sone>().apply {
+               whenever(isLocal).thenReturn(true)
+       }
+       protected val remoteSone = mock<Sone>()
+
+       protected abstract fun createCommand(core: Core): AbstractSoneCommand
+
+       @Before
+       fun setupCore() {
+               whenever(core.getSone(anyString())).thenReturn(absent())
+               whenever(core.getPost(anyString())).thenReturn(absent())
+               whenever(core.getPostReply(anyString())).thenReturn(absent())
+       }
+
+       protected fun createSone(id: String, name: String, firstName: String, lastName: String, time: Long) = mock<Sone>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.name).thenReturn(name)
+               whenever(profile).thenReturn(Profile(this).apply {
+                       this.firstName = firstName
+                       this.lastName = lastName
+               })
+               whenever(this.time).thenReturn(time)
+       }
+
+       protected fun createPost(id: String, sone: Sone, recipientId: String?, time: Long, text: String) = mock<Post>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.sone).thenReturn(sone)
+               whenever(this.recipientId).thenReturn(recipientId.asOptional())
+               whenever(this.time).thenReturn(time)
+               whenever(this.text).thenReturn(text)
+       }
+
+       protected fun createReply(id: String, sone: Sone, post: Post, time: Long, text: String) = mock<PostReply>().apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.sone).thenReturn(sone)
+               whenever(this.post).thenReturn(post.asOptional())
+               whenever(this.time).thenReturn(time)
+               whenever(this.text).thenReturn(text)
+       }
+
+       protected fun executeCommandAndExpectFcpException() {
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       protected fun requestWithoutAnyParameterResultsInFcpException() {
+               expectedException.expect(FcpException::class.java)
+               command.execute(parameters)
+       }
+
+       protected fun requestWithEmptySoneParameterResultsInFcpException() {
+               parameters += "Sone" to null
+               executeCommandAndExpectFcpException()
+       }
+
+       protected fun requestWithInvalidSoneParameterResultsInFcpException() {
+               parameters += "Sone" to "InvalidSoneId"
+               executeCommandAndExpectFcpException()
+       }
+
+       fun requestWithValidRemoteSoneParameterResultsInFcpException() {
+               parameters += "Sone" to "RemoteSoneId"
+               whenever(core.getSone("RemoteSoneId")).thenReturn(Optional.of(remoteSone))
+               executeCommandAndExpectFcpException()
+       }
+
+       protected operator fun SimpleFieldSet.plusAssign(keyValue: Pair<String, String?>) = putSingle(keyValue.first, keyValue.second)
+       protected fun SimpleFieldSet.parsePost(prefix: String) = parseFromSimpleFieldSet(prefix, "ID", "Sone", "Recipient", "Time", "Text")
+       protected fun SimpleFieldSet.parseReply(prefix: String) = parseFromSimpleFieldSet(prefix, "ID", "Sone", "Time", "Text")
+       protected fun SimpleFieldSet.parseSone(prefix: String) = parseFromSimpleFieldSet(prefix, "ID", "Name", "NiceName", "LastUpdated", "Followed") +
+                       (0 until this["${prefix}Field.Count"].toInt()).map {
+                               ("Field." + this["${prefix}Field.$it.Name"]) to this["${prefix}Field.$it.Value"]
+                       }
+
+       private fun SimpleFieldSet.parseFromSimpleFieldSet(prefix: String, vararg fields: String): Map<String, String?> = fields
+                       .associate { it to get(prefix + it) }
+
+       protected fun matchesPost(post: Post) = OneByOneMatcher<Map<String, String?>>().apply {
+               expect("ID", post.id) { it["ID"] }
+               expect("Sone", post.sone.id) { it["Sone"] }
+               expect("recipient", post.recipientId.orNull()) { it["Recipient"] }
+               expect("time", post.time.toString()) { it["Time"] }
+               expect("text", post.text.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n")) { it["Text"] }
+       }
+
+       protected fun matchesReply(reply: PostReply) = OneByOneMatcher<Map<String, String?>>().apply {
+               expect("ID", reply.id) { it["ID"] }
+               expect("Sone", reply.sone.id) { it["Sone"] }
+               expect("time", reply.time.toString()) { it["Time"] }
+               expect("text", reply.text.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n")) { it["Text"] }
+       }
+
+       protected fun matchesSone(sone: Sone) = OneByOneMatcher<Map<String, String?>>().apply {
+               expect("ID", sone.id) { it["ID"] }
+               expect("name", sone.name) { it["Name"] }
+               expect("last updated", sone.time.toString()) { it["LastUpdated"] }
+               expect("nice name", SoneAccessor.getNiceName(sone)) { it["NiceName"] }
+               sone.profile.fields.forEach { field ->
+                       expect("field: ${field.name}", field.value) { it["Field.${field.name}"] }
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/UnlockSoneCommandTest.kt
new file mode 100644 (file)
index 0000000..c002db3
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+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 [UnlockSoneCommand].
+ */
+class UnlockSoneCommandTest : SoneCommandTest() {
+
+       override fun createCommand(core: Core) = UnlockSoneCommand(core)
+
+       @Before
+       fun setupSones() {
+               whenever(core.getSone("RemoteSoneId")).thenReturn(remoteSone.asOptional())
+               whenever(core.getSone("LocalSoneId")).thenReturn(localSone.asOptional())
+               whenever(localSone.id).thenReturn("LocalSoneId")
+       }
+
+       @Test
+       fun `command requires write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(true))
+       }
+
+       @Test
+       fun `request without any parameters results in FCP exception`() {
+               requestWithoutAnyParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with invalid sone parameter results in FCP exception`() {
+               requestWithInvalidSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with valid remote sone parameter results in FCP exception`() {
+               requestWithValidRemoteSoneParameterResultsInFcpException()
+       }
+
+       @Test
+       fun `request with local sone parameter unlocks the sone`() {
+               parameters += "Sone" to "LocalSoneId"
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("SoneUnlocked"))
+               assertThat(replyParameters["Sone"], equalTo("LocalSoneId"))
+               verify(core).unlockSone(localSone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/fcp/VersionCommandTest.kt b/src/test/kotlin/net/pterodactylus/sone/fcp/VersionCommandTest.kt
new file mode 100644 (file)
index 0000000..e54e7aa
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.fcp
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.main.SonePlugin
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [VersionCommand].
+ */
+class VersionCommandTest : SoneCommandTest() {
+
+       override fun createCommand(core: Core) = VersionCommand(core)
+
+       @Test
+       fun `command does not require write access`() {
+               assertThat(command.requiresWriteAccess(), equalTo(false))
+       }
+
+       @Test
+       fun `command replies with the correct version information`() {
+               val replyParameters = command.execute(parameters).replyParameters
+               assertThat(replyParameters["Message"], equalTo("Version"))
+               assertThat(replyParameters["Version"], equalTo(SonePlugin.getPluginVersion().toString()))
+               assertThat(replyParameters["ProtocolVersion"], equalTo("1"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/L10nFilterTest.kt
new file mode 100644 (file)
index 0000000..39196c1
--- /dev/null
@@ -0,0 +1,67 @@
+package net.pterodactylus.sone.freenet
+
+import freenet.l10n.BaseL10n
+import freenet.l10n.BaseL10n.LANGUAGE.ENGLISH
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+
+/**
+ * Unit test for [L10nFilter].
+ */
+class L10nFilterTest {
+
+       private val webInterface = mock<WebInterface>()
+       private val filter = L10nFilter(webInterface)
+       private val templateContext = mock<TemplateContext>()
+       private val l10n = mock<BaseL10n>()
+       private val translations = mutableMapOf<String, String>()
+
+       @Before
+       fun setupWebInterface() {
+               whenever(webInterface.l10n).thenReturn(l10n)
+       }
+
+       @Before
+       fun setupL10n() {
+               whenever(l10n.selectedLanguage).thenReturn(ENGLISH)
+               whenever(l10n.getString(anyString())).then { translations[it.arguments[0]] }
+       }
+
+       @Test
+       fun `translation without parameters returns translated string`() {
+               translations["data"] = "translated data"
+               assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("translated data"))
+       }
+
+       @Test
+       fun `translation with parameters returned translated string`() {
+               translations["data"] = "translated {0,number} {1}"
+               assertThat(filter.format(templateContext, "data", mapOf("0" to 4.5, "1" to "data")), equalTo("translated 4.5 data"))
+       }
+
+       @Test
+       fun `filter processes l10n text without parameters correctly`() {
+               translations["data"] = "translated data"
+               assertThat(filter.format(templateContext, L10nText("data"), emptyMap()), equalTo("translated data"))
+       }
+
+       @Test
+       fun `filter processes l10n text with parameters correctly`() {
+               translations["data"] = "translated {0,number} {1}"
+               assertThat(filter.format(templateContext, L10nText("data", listOf(4.5, "data")), emptyMap()), equalTo("translated 4.5 data"))
+       }
+
+       @Test
+       fun `filter does not replace values if there are no parameters`() {
+               translations["data"] = "{link}"
+               assertThat(filter.format(templateContext, "data", emptyMap()), equalTo("{link}"));
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt
new file mode 100644 (file)
index 0000000..d0dc7c4
--- /dev/null
@@ -0,0 +1,78 @@
+package net.pterodactylus.sone.main
+
+import com.google.inject.Guice
+import freenet.client.HighLevelSimpleClient
+import freenet.clients.http.SessionManager
+import freenet.node.Node
+import freenet.pluginmanager.PluginRespirator
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [FreenetModule].
+ */
+class FreenetModuleTest {
+
+       private val sessionManager = mock<SessionManager>()
+       private val pluginRespirator = deepMock<PluginRespirator>().apply {
+               whenever(getSessionManager("Sone")).thenReturn(sessionManager)
+       }
+       private val node = pluginRespirator.node!!
+       private val highLevelSimpleClient = pluginRespirator.hlSimpleClient!!
+       private val module = FreenetModule(pluginRespirator)
+       private val injector = Guice.createInjector(module)
+
+       private inline fun <reified T: Any> verifySingletonInstance() {
+               val firstInstance = injector.getInstance<T>()
+               val secondInstance = injector.getInstance<T>()
+               assertThat(firstInstance, sameInstance(secondInstance))
+       }
+
+       @Test
+       fun `plugin respirator is returned correctly`() {
+               assertThat(injector.getInstance<PluginRespirator>(), sameInstance(pluginRespirator))
+       }
+
+       @Test
+       fun `plugin respirator is returned as singleton`() {
+               verifySingletonInstance<PluginRespirator>()
+       }
+
+       @Test
+       fun `node is returned correctly`() {
+               assertThat(injector.getInstance<Node>(), sameInstance(node))
+       }
+
+       @Test
+       fun `node is returned as singleton`() {
+               verifySingletonInstance<Node>()
+       }
+
+       @Test
+       fun `high level simply client is returned correctly`() {
+               assertThat(injector.getInstance<HighLevelSimpleClient>(), sameInstance(highLevelSimpleClient))
+       }
+
+       @Test
+       fun `high level simply client is returned as singleton`() {
+               verifySingletonInstance<HighLevelSimpleClient>()
+       }
+
+       @Test
+       fun `session manager is returned correctly`() {
+               assertThat(injector.getInstance<SessionManager>(), sameInstance(sessionManager))
+       }
+
+       @Test
+       fun `session manager is returned as singleton`() {
+               verifySingletonInstance<SessionManager>()
+               verify(pluginRespirator).getSessionManager("Sone")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/VersionParserTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/VersionParserTest.kt
new file mode 100644 (file)
index 0000000..f1702ce
--- /dev/null
@@ -0,0 +1,39 @@
+package net.pterodactylus.sone.main
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [parseVersion].
+ */
+class VersionParserTest {
+
+       @Test
+       fun `version from missing file can not be read`() {
+               assertThat(parseVersion("does-not-exist.yaml"), nullValue())
+       }
+
+       @Test
+       fun `custom version file can be parsed`() {
+               val version = parseVersion("custom-version.yaml")!!
+               assertThat(version.id, equalTo("some-id"))
+               assertThat(version.nice, equalTo("some-nice"))
+       }
+
+       @Test
+       fun `default version file is parsed`() {
+               val version = parseVersion()!!
+               assertThat(version.id, equalTo("43f3e1c3a0f487e37e5851a2cc72756d271c7571"))
+               assertThat(version.nice, equalTo("0.9.6-466-g43f3e1c"))
+       }
+
+       @Test
+       fun `parsed version is created correctly`() {
+               val version = parsedVersion!!
+               assertThat(version.id, equalTo("43f3e1c3a0f487e37e5851a2cc72756d271c7571"))
+               assertThat(version.nice, equalTo("0.9.6-466-g43f3e1c"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ImageAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ImageAccessorTest.kt
new file mode 100644 (file)
index 0000000..eb692ba
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+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.Before
+import org.junit.Test
+
+/**
+ * Unit test for [ImageAccessor].
+ */
+class ImageAccessorTest {
+
+       private val accessor = ImageAccessor()
+       private val album = mock<Album>()
+       private val images = listOf(mock<Image>(), mock<Image>())
+
+       @Before
+       fun setupImages() {
+               whenever(album.images).thenReturn(images)
+               images.forEach {
+                       whenever(it.album).thenReturn(album)
+               }
+       }
+
+       @Test
+       fun `accessor returns next image for first image`() {
+               assertThat(accessor.get(null, images[0], "next"), equalTo<Any>(images[1]))
+       }
+
+       @Test
+       fun `accessor returns null for next image of second image`() {
+               assertThat(accessor.get(null, images[1], "next"), nullValue())
+       }
+
+       @Test
+       fun `accessor returns previous image for second image`() {
+               assertThat(accessor.get(null, images[1], "previous"), equalTo<Any>(images[0]))
+       }
+
+       @Test
+       fun `accessor returns null for previous image of first image`() {
+               assertThat(accessor.get(null, images[0], "previous"), nullValue())
+       }
+
+       @Test
+       fun `accessor uses reflection accessor for all other members`() {
+               assertThat(accessor.get(null, images[0], "hashCode"), equalTo<Any>(images[0].hashCode()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt
new file mode 100644 (file)
index 0000000..07cba3d
--- /dev/null
@@ -0,0 +1,86 @@
+package net.pterodactylus.sone.template
+
+import com.google.inject.Guice
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.util.template.ClassPathTemplateProvider
+import net.pterodactylus.util.template.HtmlFilter
+import net.pterodactylus.util.template.TemplateContextFactory
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.notNullValue
+import org.hamcrest.Matchers.nullValue
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Element
+import org.junit.Test
+
+/**
+ * Unit test for [LinkedElementRenderFilter].
+ */
+class LinkedElementRenderFilterTest {
+
+       private val templateContextFactory = TemplateContextFactory()
+
+       init {
+               templateContextFactory.addFilter("html", HtmlFilter())
+               templateContextFactory.addProvider(ClassPathTemplateProvider(LinkedElementRenderFilter::class.java, "/templates/"))
+       }
+
+       private val filter = LinkedElementRenderFilter(templateContextFactory)
+
+       @Test
+       fun `filter returns null for objects that are not linked elements`() {
+               assertThat(filter.format(null, Any(), null), nullValue())
+       }
+
+       @Test
+       fun `filter renders empty span for not loaded elements`() {
+               val html = filter.format(null, LinkedElement("KSK@gpl.png", loading = true), emptyMap<String, Any?>()) as String
+               val spanNode = Jsoup.parseBodyFragment(html).body().child(0)
+               assertThat(spanNode.nodeName(), `is`("span"))
+               assertThat(spanNode.attr("class"), `is`("linked-element not-loaded"))
+               assertThat(spanNode.attr("title"), `is`("KSK@gpl.png"))
+               assertThat(spanNode.hasAttr("style"), `is`(false))
+               assertThat(spanNode.children().isEmpty(), `is`(true))
+       }
+
+       @Test
+       fun `filter can render linked images`() {
+               val html = filter.format(null, LinkedElement("KSK@gpl.png", properties = mapOf("type" to "image")), emptyMap<String, Any?>()) as String
+               val outerSpanNode = Jsoup.parseBodyFragment(html).body().child(0)
+               assertThat(outerSpanNode.nodeName(), `is`("span"))
+               assertThat(outerSpanNode.attr("class"), `is`("linked-element loaded"))
+               assertThat(outerSpanNode.attr("title"), `is`("KSK@gpl.png"))
+               val linkNode = outerSpanNode.child(0)
+               assertThat(linkNode.nodeName(), `is`("a"))
+               assertThat(linkNode.attr("href"), `is`("/KSK@gpl.png"))
+               val innerSpanNode = linkNode.child(0)
+               assertThat(innerSpanNode.attr("style"), `is`("background-image: url('/KSK@gpl.png')"))
+       }
+
+       @Test
+       fun `filter can render HTML pages`() {
+               val html = filter.format(null, LinkedElement("KSK@gpl.html", properties = mapOf("type" to "html", "title" to "Page Title", "description" to "This is the description.")), emptyMap<String, Any?>()) as String
+               val outerSpanNode = Jsoup.parseBodyFragment(html).body().child(0)
+               assertThat(outerSpanNode.nodeName(), equalTo("span"))
+               assertThat(outerSpanNode.attr("class"), `is`("linked-element loaded"))
+               assertThat(outerSpanNode.attr("title"), `is`("KSK@gpl.html"))
+               val linkNode = outerSpanNode.child(0)
+               assertThat(linkNode.nodeName(), equalTo("a"))
+               assertThat(linkNode.attr("href"), equalTo("/KSK@gpl.html"))
+               val divNodes = linkNode.children()
+               assertThat(divNodes.map(Element::nodeName), contains("div", "div"))
+               assertThat(divNodes.map { it.attr("class") }, contains("heading", "description"))
+               assertThat(divNodes.map(Element::text), contains("Page Title", "This is the description."))
+       }
+
+       @Test
+       fun `render filter can be created by guice`() {
+               val injector = Guice.createInjector(TemplateContextFactory::class.isProvidedByMock())
+               assertThat(injector.getInstance<LinkedElementRenderFilter>(), notNullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt
new file mode 100644 (file)
index 0000000..1e83b85
--- /dev/null
@@ -0,0 +1,247 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.ALWAYS
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.FOLLOWED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.MANUALLY_TRUSTED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.freenet.wot.Trust
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.PlainTextPart
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.emptyIterable
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+/**
+ * Unit test for [LinkedElementsFilter].
+ */
+class LinkedElementsFilterTest {
+
+       private val imageLoader = mock<ElementLoader>()
+       private val filter = LinkedElementsFilter(imageLoader)
+       private val templateContext = TemplateContext()
+       private val parameters = mutableMapOf<String, Any?>()
+       private val sone = createSone()
+       private val remoteSone = createSone("remote-id")
+       private val parts: List<Part> = listOf(
+                       PlainTextPart("text"),
+                       LinkPart("http://link", "link"),
+                       FreenetLinkPart("KSK@link", "link", false),
+                       FreenetLinkPart("KSK@loading.png", "link", false),
+                       FreenetLinkPart("KSK@link.png", "link", false)
+       )
+
+       @Before
+       fun setupSone() {
+               `when`(sone.options).thenReturn(DefaultSoneOptions())
+       }
+
+       @Before
+       fun setupImageLoader() {
+               `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedElement("KSK@link", failed = true))
+               `when`(imageLoader.loadElement("KSK@loading.png")).thenReturn(LinkedElement("KSK@loading.png", loading = true))
+               `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedElement("KSK@link.png"))
+       }
+
+       @Test
+       fun `filter does not find any image if there is no template context`() {
+               assertThat(filter.format(null, parts, parameters), emptyIterable())
+       }
+
+       @Test
+       fun `filter does not find any image if there is no current sone`() {
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter does not find any images if there is no remote sone`() {
+               sone.options.loadLinkedImages = ALWAYS
+               templateContext.set("currentSone", sone)
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter does not find any images if sone does not allow to load images`() {
+               templateContext.set("currentSone", sone)
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter finds all loaded freenet images from the sone itself`() {
+               templateContext.set("currentSone", sone)
+               parameters["sone"] = sone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter finds images if the remote sone is local`() {
+               sone.options.loadLinkedImages = MANUALLY_TRUSTED
+               templateContext.set("currentSone", sone)
+               `when`(remoteSone.isLocal).thenReturn(true)
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter does not find images if local sone requires manual trust and remote sone has not trust`() {
+               sone.options.loadLinkedImages = MANUALLY_TRUSTED
+               templateContext.set("currentSone", sone)
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter does not find images if local sone requires manual trust and remote sone has only implicit trust`() {
+               sone.options.loadLinkedImages = MANUALLY_TRUSTED
+               templateContext.set("currentSone", sone)
+               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(null, 100, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter does not find images if local sone requires manual trust and remote sone has explicit trust of zero`() {
+               sone.options.loadLinkedImages = MANUALLY_TRUSTED
+               templateContext.set("currentSone", sone)
+               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(0, null, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter finds images if local sone requires manual trust and remote sone has explicit trust of one`() {
+               sone.options.loadLinkedImages = MANUALLY_TRUSTED
+               templateContext.set("currentSone", sone)
+               `when`(remoteSone.identity.getTrust(this.sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter does not find images if local sone requires following and remote sone is not followed`() {
+           sone.options.loadLinkedImages = FOLLOWED
+               templateContext["currentSone"] = sone
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter finds images if local sone requires following and remote sone is followed`() {
+           sone.options.loadLinkedImages = FOLLOWED
+               `when`(sone.hasFriend("remote-id")).thenReturn(true)
+               templateContext["currentSone"] = sone
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter finds images if local sone requires following and remote sone is the same as the local sone`() {
+           sone.options.loadLinkedImages = FOLLOWED
+               templateContext["currentSone"] = sone
+               parameters["sone"] = sone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter finds images if following is required and remote sone is a local sone`() {
+               sone.options.loadLinkedImages = FOLLOWED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.isLocal).thenReturn(true)
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter does not find images if any trust is required and remote sone does not have any trust`() {
+           sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter does not find images if any trust is required and remote sone has implicit trust of zero`() {
+           sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 0, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter finds images if any trust is required and remote sone has implicit trust of one`() {
+           sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(null, 1, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter does not find images if any trust is required and remote sone has explicit trust of zero but implicit trust of one`() {
+               sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(0, 1, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesAreNotPresent()
+       }
+
+       @Test
+       fun `filter finds images if any trust is required and remote sone has explicit trust of one but no implicit trust`() {
+               sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.identity.getTrust(sone.identity as OwnIdentity)).thenReturn(Trust(1, null, null))
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter finds images if any trust is required and remote sone is a local sone`() {
+               sone.options.loadLinkedImages = TRUSTED
+               templateContext["currentSone"] = sone
+               `when`(remoteSone.isLocal).thenReturn(true)
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       @Test
+       fun `filter finds images if no trust is required`() {
+           sone.options.loadLinkedImages = ALWAYS
+               templateContext["currentSone"] = sone
+               parameters["sone"] = remoteSone
+               verifyThatImagesArePresent()
+       }
+
+       private fun verifyThatImagesArePresent() {
+               val loadedImages = filter.format(templateContext, parts, parameters)
+               assertThat(loadedImages, contains<LinkedElement>(
+                               LinkedElement("KSK@loading.png", failed = false, loading = true),
+                               LinkedElement("KSK@link.png", failed = false, loading = false)
+               ))
+       }
+
+       private fun verifyThatImagesAreNotPresent() {
+               assertThat(filter.format(templateContext, parts, parameters), emptyIterable())
+       }
+
+       private fun createSone(id: String = "sone-id"): Sone {
+               val sone = mock<Sone>()
+               `when`(sone.id).thenReturn(id)
+               `when`(sone.options).thenReturn(DefaultSoneOptions())
+               `when`(sone.identity).thenReturn(mock<OwnIdentity>())
+               return sone
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ParserFilterTest.kt
new file mode 100644 (file)
index 0000000..ac2886a
--- /dev/null
@@ -0,0 +1,91 @@
+package net.pterodactylus.sone.template
+
+import com.google.common.base.Optional.of
+import com.google.inject.Guice
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.SoneTextParser
+import net.pterodactylus.sone.text.SoneTextParserContext
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.emptyIterable
+import org.hamcrest.Matchers.notNullValue
+import org.hamcrest.Matchers.sameInstance
+import org.junit.Test
+import org.mockito.ArgumentCaptor.forClass
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [ParserFilter].
+ */
+class ParserFilterTest {
+
+       companion object {
+               private const val SONE_IDENTITY = "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"
+       }
+
+       private val core = mock<Core>()
+       private val sone = setupSone(SONE_IDENTITY)
+       private val soneTextParser = mock<SoneTextParser>()
+       private val templateContext = TemplateContext()
+       private val parameters = mutableMapOf<String, Any?>()
+       private val filter = ParserFilter(core, soneTextParser)
+
+       private fun setupSone(identity: String): Sone {
+               val sone = mock<Sone>()
+               `when`(sone.id).thenReturn(identity)
+               `when`(core.getSone(identity)).thenReturn(of(sone))
+               return sone
+       }
+
+       @Test
+       fun `parsing null returns an empty iterable`() {
+               assertThat(filter.format(templateContext, null, mutableMapOf()) as Iterable<*>, emptyIterable())
+       }
+
+       @Test
+       fun `given sone is used to create parser context`() {
+               setupSoneAndVerifyItIsUsedInContext(sone, sone)
+       }
+
+       @Test
+       fun `sone with given sone ID is used to create parser context`() {
+               setupSoneAndVerifyItIsUsedInContext(SONE_IDENTITY, sone)
+       }
+
+       private fun setupSoneAndVerifyItIsUsedInContext(soneOrSoneId: Any, sone: Sone) {
+               parameters.put("sone", soneOrSoneId)
+               filter.format(templateContext, "text", parameters)
+               val context = forClass(SoneTextParserContext::class.java)
+               verify(soneTextParser).parse(eq<String>("text"), context.capture())
+               assertThat(context.value.postingSone, `is`(sone))
+       }
+
+       @Test
+       fun `parser filter can be created by guice`() {
+           val injector = Guice.createInjector(
+                           Core::class.isProvidedByMock(),
+                           SoneTextParser::class.isProvidedByMock()
+           )
+               assertThat(injector.getInstance<ParserFilter>(), notNullValue())
+       }
+
+       @Test
+       fun `parser filter is created as singleton`() {
+               val injector = Guice.createInjector(
+                               Core::class.isProvidedByMock(),
+                               SoneTextParser::class.isProvidedByMock()
+               )
+               val firstInstance = injector.getInstance<ParserFilter>()
+               val secondInstance = injector.getInstance<ParserFilter>()
+               assertThat(firstInstance, sameInstance(secondInstance))
+
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ProfileAccessorTest.kt
new file mode 100644 (file)
index 0000000..717d3c5
--- /dev/null
@@ -0,0 +1,188 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.ALWAYS
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.FOLLOWED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.MANUALLY_TRUSTED
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.NEVER
+import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent.TRUSTED
+import net.pterodactylus.sone.freenet.wot.Identity
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.freenet.wot.Trust
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.nullValue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+
+/**
+ * Unit test for [ProfileAccessor].
+ */
+class ProfileAccessorTest {
+
+       private val core = mock<Core>()
+       private val accessor = ProfileAccessor(core)
+       private val profile = mock<Profile>()
+       private val templateContext = mock<TemplateContext>()
+       private val currentSone = mock<Sone>()
+       private val remoteSone = mock<Sone>()
+
+       @Before
+       fun setupTemplateContext() {
+               whenever(templateContext.get("currentSone")).thenReturn(currentSone)
+       }
+
+       @Before
+       fun setupProfile() {
+               whenever(profile.sone).thenReturn(remoteSone)
+               whenever(profile.avatar).thenReturn("avatar-id")
+       }
+
+       @Before
+       fun setupSones() {
+               val currentIdentity = mock<OwnIdentity>()
+               whenever(currentSone.options).thenReturn(DefaultSoneOptions())
+               whenever(currentSone.identity).thenReturn(currentIdentity)
+               whenever(remoteSone.id).thenReturn("remote-sone")
+               val identity = mock<Identity>()
+               val trust = Trust(null, null, null)
+               whenever(remoteSone.identity).thenReturn(identity)
+               whenever(identity.getTrust(currentIdentity)).thenReturn(trust)
+       }
+
+       @Before
+       fun setupCore() {
+               whenever(core.getImage(eq("avatar-id"), anyBoolean())).thenReturn(mock<Image>())
+       }
+
+
+       @Test
+       fun `avatar is null if there is no current sone`() {
+               whenever(templateContext.get("currentSone")).thenReturn(null)
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar is null if profile has no avatar id`() {
+               whenever(profile.avatar).thenReturn(null)
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar is null if core has no image with avatar ID`() {
+               whenever(core.getImage(eq("avatar-id"), anyBoolean())).thenReturn(null)
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar ID is returned if profile belongs to local sone`() {
+               whenever(remoteSone.isLocal).thenReturn(true)
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to never show avatars`() {
+               currentSone.options.showCustomAvatars = NEVER
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar ID is returned if sone is configure to always show avatars`() {
+               currentSone.options.showCustomAvatars = ALWAYS
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `avatar ID is returned if sone is configure to show avatars of followed sones and remote sone is followed`() {
+               currentSone.options.showCustomAvatars = FOLLOWED
+               whenever(currentSone.hasFriend("remote-sone")).thenReturn(true)
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars of followed sones but remote sone is not followed`() {
+               currentSone.options.showCustomAvatars = FOLLOWED
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on trust but there is no trust`() {
+               setTrust(null)
+               currentSone.options.showCustomAvatars = MANUALLY_TRUSTED
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       private fun setTrust(trust: Trust?) {
+               whenever(remoteSone.identity.getTrust(currentSone.identity as OwnIdentity)).thenReturn(trust)
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on manual trust but there is no explicit trust`() {
+               currentSone.options.showCustomAvatars = MANUALLY_TRUSTED
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on manual trust but explicit trust is zero`() {
+               currentSone.options.showCustomAvatars = MANUALLY_TRUSTED
+               setTrust(Trust(0, null, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar ID is returned if sone is configure to show avatars based on manual trust and explicit trust is one`() {
+               currentSone.options.showCustomAvatars = MANUALLY_TRUSTED
+               setTrust(Trust(1, null, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on trust but explicit trust is zero`() {
+               currentSone.options.showCustomAvatars = TRUSTED
+               setTrust(Trust(0, null, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar ID is returned if sone is configure to show avatars based on trust and explicit trust is one`() {
+               currentSone.options.showCustomAvatars = TRUSTED
+               setTrust(Trust(1, null, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on trust but both explicit and implicit trust are null`() {
+               currentSone.options.showCustomAvatars = TRUSTED
+               setTrust(Trust(null, null, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar is null if sone is configure to show avatars based on trust but both implicit trust is zero`() {
+               currentSone.options.showCustomAvatars = TRUSTED
+               setTrust(Trust(null, 0, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), nullValue())
+       }
+
+       @Test
+       fun `avatar ID is returned if sone is configure to show avatars based on trust and implicit trust is one`() {
+               currentSone.options.showCustomAvatars = TRUSTED
+               setTrust(Trust(0, 1, null))
+               assertThat(accessor.get(templateContext, profile, "avatar"), `is`<Any>("avatar-id"))
+       }
+
+       @Test
+       fun `accessing other members uses reflection accessor`() {
+               assertThat(accessor.get(templateContext, profile, "hashCode"), `is`<Any>(profile.hashCode()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/RenderFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/RenderFilterTest.kt
new file mode 100644 (file)
index 0000000..b8d6259
--- /dev/null
@@ -0,0 +1,156 @@
+package net.pterodactylus.sone.template
+
+import com.google.common.base.Optional
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.FreemailPart
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.PlainTextPart
+import net.pterodactylus.sone.text.PostPart
+import net.pterodactylus.sone.text.SonePart
+import net.pterodactylus.util.template.HtmlFilter
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Attribute
+import org.jsoup.nodes.Element
+import org.junit.Test
+import org.mockito.Mockito.`when`
+import java.net.URLEncoder
+
+/**
+ * Unit test for [RenderFilter].
+ */
+class RenderFilterTest {
+
+       companion object {
+               private const val FREEMAIL_ID = "t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra"
+               private const val SONE_FREEMAIL = "sone@$FREEMAIL_ID.freemail"
+               private const val SONE_IDENTITY = "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"
+               private const val POST_ID = "37a06250-6775-4b94-86ff-257ba690953c"
+       }
+
+       private val core = mock<Core>()
+       private val templateContextFactory = TemplateContextFactory()
+       private val templateContext: TemplateContext
+       private val sone = setupSone(SONE_IDENTITY, "Sone", "First")
+       private val parameters = mutableMapOf<String, Any?>()
+
+       init {
+               templateContextFactory.addFilter("html", HtmlFilter())
+               templateContext = templateContextFactory.createTemplateContext()
+       }
+
+       private val filter = RenderFilter(core, templateContextFactory)
+
+       @Test
+       fun `plain text part is rendered correctly`() {
+               assertThat(renderParts(PlainTextPart("plain text")), `is`("plain text"))
+       }
+
+       private fun renderParts(vararg part: Part) = filter.format(templateContext, listOf(*part), parameters) as String
+
+       @Test
+       fun `freenet link is rendered correctly`() {
+               val linkNode = renderParts(FreenetLinkPart("KSK@gpl.txt", "gpl.txt", false)).toLinkNode()
+               verifyLink(linkNode, "/KSK@gpl.txt", "freenet", "KSK@gpl.txt", "gpl.txt")
+       }
+
+       private fun verifyLink(linkNode: Element, url: String, cssClass: String, tooltip: String, text: String) {
+               assertThat(linkNode.nodeName(), `is`("a"))
+               assertThat<List<Attribute>>(linkNode.attributes().asList(), containsInAnyOrder(
+                               Attribute("href", url),
+                               Attribute("class", cssClass),
+                               Attribute("title", tooltip)
+               ))
+               assertThat(linkNode.text(), `is`(text))
+       }
+
+       @Test
+       fun `trusted freenet link is rendered with correct css class`() {
+               val linkNode = renderParts(FreenetLinkPart("KSK@gpl.txt", "gpl.txt", true)).toLinkNode()
+               verifyLink(linkNode, "/KSK@gpl.txt", "freenet-trusted", "KSK@gpl.txt", "gpl.txt")
+       }
+
+       private fun String.toLinkNode() = Jsoup.parseBodyFragment(this).body().child(0)
+
+       @Test
+       fun `internet link is rendered correctly`() {
+               val linkNode = renderParts(LinkPart("http://test.com/test.html", "test.com/test.html")).toLinkNode()
+               verifyLink(linkNode, "/external-link/?_CHECKED_HTTP_=${URLEncoder.encode("http://test.com/test.html", "UTF-8")}", "internet",
+                               "http://test.com/test.html", "test.com/test.html")
+       }
+
+       @Test
+       fun `sone parts are rendered correctly`() {
+               val linkNode = renderParts(SonePart(sone)).toLinkNode()
+               verifyLink(linkNode, "viewSone.html?sone=" + SONE_IDENTITY, "in-sone", "First", "First")
+       }
+
+       private fun setupSone(identity: String, name: String?, firstName: String): Sone {
+               val sone = mock<Sone>()
+               `when`(sone.id).thenReturn(identity)
+               `when`(sone.profile).thenReturn(Profile(sone))
+               `when`(sone.name).thenReturn(name)
+               sone.profile.firstName = firstName
+               `when`(core.getSone(identity)).thenReturn(Optional.of<Sone>(sone))
+               return sone
+       }
+
+       @Test
+       fun `sone part with unknown sone is rendered as link to web of trust`() {
+               val sone = setupSone(SONE_IDENTITY, null, "First")
+               val linkNode = renderParts(SonePart(sone)).toLinkNode()
+               verifyLink(linkNode, "/WebOfTrust/ShowIdentity?id=$SONE_IDENTITY", "in-sone", SONE_IDENTITY, SONE_IDENTITY)
+       }
+
+       @Test
+       fun `post part is cut off correctly when there are spaces`() {
+               val post = setupPost(sone, "1234 678901 345 789012 45678 01.")
+               val linkNode = renderParts(PostPart(post)).toLinkNode()
+               verifyLink(linkNode, "viewPost.html?post=$POST_ID", "in-sone", "First", "1234 678901 345…")
+       }
+
+       private fun setupPost(sone: Sone, value: String): Post {
+               val post = mock<Post>()
+               `when`(post.id).thenReturn(POST_ID)
+               `when`(post.sone).thenReturn(sone)
+               `when`(post.text).thenReturn(value)
+               return post
+       }
+
+       @Test
+       fun `post part is cut off correctly when there are no spaces`() {
+               val post = setupPost(sone, "1234567890123456789012345678901.")
+               val linkNode = renderParts(PostPart(post)).toLinkNode()
+               verifyLink(linkNode, "viewPost.html?post=$POST_ID", "in-sone", "First", "12345678901234567890…")
+       }
+
+       @Test
+       fun `post part shorter than 21 chars is not cut off`() {
+               val post = setupPost(sone, "12345678901234567890")
+               val linkNode = renderParts(PostPart(post)).toLinkNode()
+               verifyLink(linkNode, "viewPost.html?post=$POST_ID", "in-sone", "First", "12345678901234567890")
+       }
+
+       @Test
+       fun `multiple parts are rendered correctly`() {
+               val parts = arrayOf(PlainTextPart("te"), PlainTextPart("xt"))
+               assertThat(renderParts(*parts), `is`("text"))
+       }
+
+       @Test
+       fun `freemail address is displayed correctly`() {
+               val linkNode = renderParts(FreemailPart("sone", FREEMAIL_ID, SONE_IDENTITY)).toLinkNode()
+               verifyLink(linkNode, "/Freemail/NewMessage?to=$SONE_IDENTITY", "in-sone", "First\n$SONE_FREEMAIL", "sone@First.freemail")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ReplyAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ReplyAccessorTest.kt
new file mode 100644 (file)
index 0000000..f06069b
--- /dev/null
@@ -0,0 +1,86 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.Core
+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.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [ReplyAccessor].
+ */
+class ReplyAccessorTest {
+
+       private val core = mock<Core>()
+       private val accessor = ReplyAccessor(core)
+       private val templateContext = mock<TemplateContext>()
+       private val reply = mock<PostReply>()
+       private val currentSone = mock<Sone>()
+
+       @Before
+       fun setupReply() {
+               whenever(reply.id).thenReturn("reply-id")
+       }
+
+       @Before
+       fun setupTemplateContext() {
+               whenever(templateContext.get("currentSone")).thenReturn(currentSone)
+       }
+
+       @Test
+       fun `returns the likes correctly`() {
+               val sones = setOf(mock<Sone>(), mock<Sone>(), mock<Sone>())
+               whenever(core.getLikes(reply)).thenReturn(sones)
+               assertThat(accessor.get(templateContext, reply, "likes"), equalTo<Any>(sones))
+       }
+
+       @Test
+       fun `returns that the reply is not liked if the current sone is null`() {
+               whenever(templateContext.get("currentSone")).thenReturn(null)
+               assertThat(accessor.get(templateContext, reply, "liked"), equalTo<Any>(false))
+       }
+
+       @Test
+       fun `returns that the reply is not liked if the current sone does not like the reply`() {
+               assertThat(accessor.get(templateContext, reply, "liked"), equalTo<Any>(false))
+       }
+
+       @Test
+       fun `returns that the reply is liked if the current sone does like the reply`() {
+               whenever(currentSone.isLikedReplyId("reply-id")).thenReturn(true)
+               assertThat(accessor.get(templateContext, reply, "liked"), equalTo<Any>(true))
+       }
+
+       @Test
+       fun `returns that the reply is new if the reply is not known`() {
+               assertThat(accessor.get(templateContext, reply, "new"), equalTo<Any>(true))
+       }
+
+       @Test
+       fun `returns that the reply is not new if the reply is known`() {
+               whenever(reply.isKnown).thenReturn(true)
+               assertThat(accessor.get(templateContext, reply, "new"), equalTo<Any>(false))
+       }
+
+       @Test
+       fun `return that a reply is not loaded if its sone is null`() {
+               assertThat(accessor.get(templateContext, reply, "loaded"), equalTo<Any>(false))
+       }
+
+       @Test
+       fun `return that a reply is loaded if its sone is not null`() {
+               whenever(reply.sone).thenReturn(mock<Sone>())
+               assertThat(accessor.get(templateContext, reply, "loaded"), equalTo<Any>(true))
+       }
+
+       @Test
+       fun `reflection accessor is used for all other members`() {
+               assertThat(accessor.get(templateContext, reply, "hashCode"), equalTo<Any>(reply.hashCode()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ReplyGroupFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ReplyGroupFilterTest.kt
new file mode 100644 (file)
index 0000000..4614454
--- /dev/null
@@ -0,0 +1,64 @@
+package net.pterodactylus.sone.template
+
+import com.google.common.base.Optional
+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.test.whenever
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * Unit test for [ReplyGroupFilter].
+ */
+class ReplyGroupFilterTest {
+
+       private val filter = ReplyGroupFilter()
+       private val replies = mutableListOf<PostReply>()
+       private val posts = mutableListOf<Post>()
+       private val sones = mutableListOf<Sone>()
+       private val templateContext = mock<TemplateContext>()
+
+       @Before
+       fun setupReplies() {
+               (0..4).forEach {
+                       sones += mock<Sone>()
+               }
+               (0..7).forEach {
+                       posts += mock<Post>()
+                       whenever(posts[it].sone).thenReturn(sones[(it + 1) % sones.size])
+               }
+               (0..10).forEach {
+                       replies += mock<PostReply>()
+                       whenever(replies[it].sone).thenReturn(sones[it % sones.size])
+                       whenever(replies[it].post).thenReturn(Optional.of(posts[it % posts.size]))
+               }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `replies are grouped correctly`() {
+               val groupReplies = filter.format(templateContext, replies, emptyMap()) as Map<Post, Map<String, *>>
+               assertThat(groupReplies.keys, equalTo(posts.toSet()))
+               verifyPostRepliesAndSones(groupReplies, 0, listOf(0, 3), listOf(0, 8))
+               verifyPostRepliesAndSones(groupReplies, 1, listOf(1, 4), listOf(1, 9))
+               verifyPostRepliesAndSones(groupReplies, 2, listOf(2, 0), listOf(2, 10))
+               verifyPostRepliesAndSones(groupReplies, 3, listOf(3), listOf(3))
+               verifyPostRepliesAndSones(groupReplies, 4, listOf(4), listOf(4))
+               verifyPostRepliesAndSones(groupReplies, 5, listOf(0), listOf(5))
+               verifyPostRepliesAndSones(groupReplies, 6, listOf(1), listOf(6))
+               verifyPostRepliesAndSones(groupReplies, 7, listOf(2), listOf(7))
+       }
+
+       @Suppress("UNCHECKED_CAST")
+       private fun verifyPostRepliesAndSones(groupReplies: Map<Post, Map<String, *>>, postIndex: Int, soneIndices: List<Int>, replyIndices: List<Int>) {
+               assertThat(groupReplies[posts[postIndex]]!!["sones"] as Iterable<Sone>, containsInAnyOrder(*soneIndices.map { sones[it] }.toTypedArray()))
+               assertThat(groupReplies[posts[postIndex]]!!["replies"] as Iterable<PostReply>, containsInAnyOrder(*replyIndices.map { replies[it] }.toTypedArray()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/RequestChangeFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/RequestChangeFilterTest.kt
new file mode 100644 (file)
index 0000000..c51cf27
--- /dev/null
@@ -0,0 +1,71 @@
+package net.pterodactylus.sone.template
+
+import freenet.support.api.HTTPRequest
+import net.pterodactylus.sone.test.Matchers.matchesRegex
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import java.net.URI
+
+/**
+ * Unit test for [RequestChangeFilter].
+ */
+class RequestChangeFilterTest {
+
+       private val filter = RequestChangeFilter()
+       private val templateContext = mock<TemplateContext>()
+       private val freenetRequest = mock<FreenetRequest>()
+       private val httpRequest = mock<HTTPRequest>()
+       private val parameters = mutableMapOf<String, String>()
+
+       @Before
+       fun setupFreenetRequest() {
+               whenever(freenetRequest.httpRequest).thenReturn(httpRequest)
+               whenever(freenetRequest.httpRequest.parameterNames).thenAnswer { parameters.keys }
+               whenever(freenetRequest.httpRequest.getParam(anyString())).thenAnswer { parameters[it.arguments[0]] }
+       }
+
+       @Test
+       fun `filter correctly appends parameter to request URL without parameters`() {
+               whenever(freenetRequest.uri).thenReturn(URI("/some/path.html"))
+               val uri = filter.format(templateContext, freenetRequest, mapOf("name" to "name", "value" to "value")) as URI
+               assertThat(uri, equalTo(URI("/some/path.html?name=value")))
+       }
+
+       @Test
+       fun `filter cuts off old query`() {
+               whenever(freenetRequest.uri).thenReturn(URI("/some/path.html?foo=bar"))
+               val uri = filter.format(templateContext, freenetRequest, mapOf("name" to "name", "value" to "value")) as URI
+               assertThat(uri, equalTo(URI("/some/path.html?name=value")))
+       }
+
+       @Test
+       fun `filter correctly appends parameter to request URL with parameters`() {
+               parameters["foo"] = "bar"
+               whenever(freenetRequest.uri).thenReturn(URI("/some/path.html"))
+               val uri = filter.format(templateContext, freenetRequest, mapOf("name" to "name", "value" to "value")) as URI
+               assertThat(uri.toString(), matchesRegex("/some/path.html\\?(foo=bar&name=value|name=value&foo=bar)"))
+       }
+
+       @Test
+       fun `filter overwrites existing parameter value`() {
+               parameters["name"] = "old"
+               whenever(freenetRequest.uri).thenReturn(URI("/some/path.html"))
+               val uri = filter.format(templateContext, freenetRequest, mapOf("name" to "name", "value" to "value")) as URI
+               assertThat(uri, equalTo(URI("/some/path.html?name=value")))
+       }
+
+       @Test
+       fun `filter correctly encodes characters`() {
+               whenever(freenetRequest.uri).thenReturn(URI("/some/path.html"))
+               val uri = filter.format(templateContext, freenetRequest, mapOf("name" to "name", "value" to " välue")) as URI
+               assertThat(uri, equalTo(URI("/some/path.html?name=+v%C3%A4lue")))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/ShortenFilterTest.kt
new file mode 100644 (file)
index 0000000..c6990a7
--- /dev/null
@@ -0,0 +1,96 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.sone.text.PlainTextPart
+import net.pterodactylus.sone.text.SonePart
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+/**
+ * Unit test for [ShortenFilter].
+ */
+class ShortenFilterTest {
+
+       private val filter = ShortenFilter()
+
+       @Suppress("UNCHECKED_CAST")
+       private fun shortenParts(length: Int, cutOffLength: Int, vararg parts: Part) =
+                       filter.format(null, listOf(*parts), mapOf("cut-off-length" to cutOffLength, "length" to length)) as Iterable<Part>
+
+       @Test
+       fun `plain text part is shortened if length exceeds maxl ength`() {
+               assertThat(shortenParts(15, 10, PlainTextPart("This is a long text.")), contains<Part>(
+                               PlainTextPart("This is a …")
+               ))
+       }
+
+       @Test
+       fun `plain text part is not shortened if length does not exceed max length`() {
+               assertThat(shortenParts(20, 10, PlainTextPart("This is a long text.")), contains<Part>(
+                               PlainTextPart("This is a long text.")
+               ))
+       }
+
+       @Test
+       fun `short parts are not shortened`() {
+               assertThat(shortenParts(15, 10, PlainTextPart("This.")), contains<Part>(
+                               PlainTextPart("This.")
+               ))
+       }
+
+       @Test
+       fun `multiple plain text parts are shortened`() {
+               assertThat(shortenParts(15, 10, PlainTextPart("This "), PlainTextPart("is a long text.")), contains<Part>(
+                               PlainTextPart("This "),
+                               PlainTextPart("is a …")
+               ))
+       }
+
+       @Test
+       fun `parts after length has been reached are ignored`() {
+               assertThat(shortenParts(15, 10, PlainTextPart("This is a long text."), PlainTextPart(" And even more.")), contains<Part>(
+                               PlainTextPart("This is a …")
+               ))
+       }
+
+       @Test
+       fun `link parts are not shortened`() {
+               assertThat(shortenParts(15, 10, FreenetLinkPart("KSK@gpl.txt", "This is a long text.", false)), contains<Part>(
+                               FreenetLinkPart("KSK@gpl.txt", "This is a long text.", false)
+               ))
+       }
+
+       @Test
+       fun `additional link parts are ignored`() {
+               assertThat(shortenParts(15, 10, PlainTextPart("This is a long text."), FreenetLinkPart("KSK@gpl.txt", "This is a long text.", false)), contains<Part>(
+                               PlainTextPart("This is a …")
+               ))
+       }
+
+       @Test
+       fun `sone parts are added but their length is ignored`() {
+               val sone = mock<Sone>()
+               `when`(sone.profile).thenReturn(Profile(sone))
+               assertThat(shortenParts(15, 10, SonePart(sone), PlainTextPart("This is a long text.")), contains<Part>(
+                               SonePart(sone),
+                               PlainTextPart("This is a …")
+               ))
+       }
+
+       @Test
+       fun `additional sone parts are ignored`() {
+               val sone = mock<Sone>()
+               `when`(sone.profile).thenReturn(Profile(sone))
+               assertThat(shortenParts(15, 10, PlainTextPart("This is a long text."), SonePart(sone)), contains<Part>(
+                               PlainTextPart("This is a …")
+               ))
+       }
+
+}
+
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/SoneAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/SoneAccessorTest.kt
new file mode 100644 (file)
index 0000000..2fe40d4
--- /dev/null
@@ -0,0 +1,247 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.Sone.SoneStatus
+import net.pterodactylus.sone.data.Sone.SoneStatus.downloading
+import net.pterodactylus.sone.data.Sone.SoneStatus.idle
+import net.pterodactylus.sone.data.Sone.SoneStatus.inserting
+import net.pterodactylus.sone.data.Sone.SoneStatus.unknown
+import net.pterodactylus.sone.freenet.L10nText
+import net.pterodactylus.sone.freenet.wot.Identity
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.freenet.wot.Trust
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.text.TimeText
+import net.pterodactylus.sone.text.TimeTextConverter
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.Matcher
+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 [SoneAccessor].
+ */
+class SoneAccessorTest {
+
+       private val core = mock<Core>()
+       private val timeTextConverter = mock<TimeTextConverter>()
+       private val accessor = SoneAccessor(core, timeTextConverter)
+       private val templateContext = mock<TemplateContext>()
+       private val currentSone = mock<Sone>()
+       private val currentIdentity = mock<OwnIdentity>()
+       private val sone = mock<Sone>()
+       private val remoteIdentity = mock<Identity>()
+
+       @Before
+       fun setupSone() {
+               whenever(sone.id).thenReturn("sone-id")
+               whenever(sone.name).thenReturn("sone-name")
+               whenever(sone.profile).thenReturn(Profile(sone))
+               whenever(sone.identity).thenReturn(remoteIdentity)
+               whenever(currentSone.identity).thenReturn(currentIdentity)
+       }
+
+       @Before
+       fun setupTemplateContext() {
+               whenever(templateContext["currentSone"]).thenReturn(currentSone)
+       }
+
+       private fun assertAccessorReturnValue(member: String, expected: Any?) {
+               assertThat(accessor.get(templateContext, sone, member), equalTo(expected))
+       }
+
+       @Suppress("UNCHECKED_CAST")
+       private fun <T : Any> assertAccessorReturnValueMatches(member: String, matcher: Matcher<in T>) {
+               assertThat(accessor.get(templateContext, sone, member) as T, matcher)
+       }
+
+       @Test
+       fun `accessor returns nice name of a sone`() {
+               assertAccessorReturnValue("niceName", "sone-name")
+       }
+
+       @Test
+       fun `accessor returns that given sone is not a friend of the current sone if there is no current sone`() {
+               whenever(templateContext["currentSone"]).thenReturn(null)
+               assertAccessorReturnValue("friend", false)
+       }
+
+       @Test
+       fun `accessor returns that given sone is not a friend of the current sone if the given sone is not a friend`() {
+               assertAccessorReturnValue("friend", false)
+       }
+
+       @Test
+       fun `accessor returns that given sone is a friend of the current sone if the given sone is a friend`() {
+               whenever(currentSone.hasFriend("sone-id")).thenReturn(true)
+               assertAccessorReturnValue("friend", true)
+       }
+
+       @Test
+       fun `accessor returns that the given sone is not the current sone if there is no current sone`() {
+               whenever(templateContext["currentSone"]).thenReturn(null)
+               assertAccessorReturnValue("current", false)
+       }
+
+       @Test
+       fun `accessor returns that the given sone is not the current sone if it is not`() {
+               assertAccessorReturnValue("current", false)
+       }
+
+       @Test
+       fun `accessor returns that the given sone is the current sone if it is `() {
+               whenever(templateContext["currentSone"]).thenReturn(sone)
+               assertAccessorReturnValue("current", true)
+       }
+
+       @Test
+       fun `accessor returns that a sone was not modified if the sone was not modified`() {
+               assertAccessorReturnValue("modified", false)
+       }
+
+       @Test
+       fun `accessor returns that a sone was modified if the sone was modified`() {
+               whenever(core.isModifiedSone(sone)).thenReturn(true)
+               assertAccessorReturnValue("modified", true)
+       }
+
+       @Test
+       fun `accessor returns the sone’s status`() {
+               val soneStatus = mock<SoneStatus>()
+               whenever(sone.status).thenReturn(soneStatus)
+               assertAccessorReturnValue("status", soneStatus)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is unknown if it is unknown`() {
+               whenever(sone.status).thenReturn(unknown)
+               assertAccessorReturnValue("unknown", true)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is not unknown if it is not unknown`() {
+               whenever(sone.status).thenReturn(mock<SoneStatus>())
+               assertAccessorReturnValue("unknown", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is idle if it is idle`() {
+               whenever(sone.status).thenReturn(idle)
+               assertAccessorReturnValue("idle", true)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is not idle if it is not idle`() {
+               whenever(sone.status).thenReturn(mock<SoneStatus>())
+               assertAccessorReturnValue("idle", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is inserting if it is inserting`() {
+               whenever(sone.status).thenReturn(inserting)
+               assertAccessorReturnValue("inserting", true)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is not inserting if it is not inserting`() {
+               whenever(sone.status).thenReturn(mock<SoneStatus>())
+               assertAccessorReturnValue("inserting", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is downloading if it is downloading`() {
+               whenever(sone.status).thenReturn(downloading)
+               assertAccessorReturnValue("downloading", true)
+       }
+
+       @Test
+       fun `accessor returns that the sone’s status is not downloading if it is not downloading`() {
+               whenever(sone.status).thenReturn(mock<SoneStatus>())
+               assertAccessorReturnValue("downloading", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone is new if it is not known`() {
+               assertAccessorReturnValue("new", true)
+       }
+
+       @Test
+       fun `accessor returns that the sone is not new if it is known`() {
+               whenever(sone.isKnown).thenReturn(true)
+               assertAccessorReturnValue("new", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone is not locked if it is not locked`() {
+               assertAccessorReturnValue("locked", false)
+       }
+
+       @Test
+       fun `accessor returns that the sone is locked if it is locked`() {
+               whenever(core.isLocked(sone)).thenReturn(true)
+               assertAccessorReturnValue("locked", true)
+       }
+
+       @Test
+       fun `accessor returns l10n text for last update time`() {
+               whenever(sone.time).thenReturn(12345)
+               whenever(timeTextConverter.getTimeText(12345L)).thenReturn(TimeText(L10nText("l10n.key", listOf(3L)), 23456))
+               assertAccessorReturnValue("lastUpdatedText", L10nText("l10n.key", listOf(3L)))
+       }
+
+       @Test
+       fun `accessor returns null trust if there is no current sone`() {
+               whenever(templateContext["currentSone"]).thenReturn(null)
+               assertAccessorReturnValue("trust", null)
+       }
+
+       @Test
+       fun `accessor returns trust with null values if there is no trust from the current sone`() {
+               assertAccessorReturnValue("trust", Trust(null, null, null))
+       }
+
+       @Test
+       fun `accessor returns trust if there is trust from the current sone`() {
+               val trust = mock<Trust>()
+               whenever(remoteIdentity.getTrust(currentIdentity)).thenReturn(trust)
+               assertAccessorReturnValue("trust", trust)
+       }
+
+       @Test
+       fun `accessor returns all images in the correct order`() {
+               val images = listOf(mock<Image>(), mock<Image>(), mock<Image>(), mock<Image>(), mock<Image>())
+               val firstAlbum = createAlbum(listOf(), listOf(images[0], images[3]))
+               val secondAlbum = createAlbum(listOf(), listOf(images[1], images[4], images[2]))
+               val rootAlbum = createAlbum(listOf(firstAlbum, secondAlbum), listOf())
+               whenever(sone.rootAlbum).thenReturn(rootAlbum)
+               assertAccessorReturnValueMatches("allImages", contains(images[0], images[3], images[1], images[4], images[2]))
+       }
+
+       private fun createAlbum(albums: List<Album>, images: List<Image>) =
+                       mock<Album>().apply {
+                               whenever(this.albums).thenReturn(albums)
+                               whenever(this.images).thenReturn(images)
+                       }
+
+       @Test
+       fun `accessor returns all albums in the correct order`() {
+               val albums = listOf(mock<Album>(), mock<Album>(), mock<Album>(), mock<Album>(), mock<Album>())
+               val rootAlbum = createAlbum(albums, listOf())
+               whenever(sone.rootAlbum).thenReturn(rootAlbum)
+               assertAccessorReturnValueMatches("albums", contains(*albums.toTypedArray()))
+       }
+
+       @Test
+       fun `reflection accessor is used for other members`() {
+           assertAccessorReturnValue("hashCode", sone.hashCode())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/SubstringFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/SubstringFilterTest.kt
new file mode 100644 (file)
index 0000000..eebfe3c
--- /dev/null
@@ -0,0 +1,62 @@
+package net.pterodactylus.sone.template
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [SubstringFilter].
+ */
+class SubstringFilterTest {
+
+       private val filter = SubstringFilter()
+       private val string = "abcdefghijklmnopqrstuvwxyz"
+
+       private fun filterText(vararg parameters: Pair<String, Int>) = filter.format(null, string, mapOf(*parameters))
+
+       @Test
+       fun `filter returns the input string when no parameters are given`() {
+               assertThat(filterText(), equalTo<Any>(string))
+       }
+
+       @Test
+       fun `filter returns "abc" if start is omitted and length is three`() {
+           assertThat(filterText("length" to 3), equalTo<Any>("abc"))
+       }
+
+       @Test
+       fun `filter returns complete string if length is larger than length of string`() {
+           assertThat(filterText("length" to 3000), equalTo<Any>(string))
+       }
+
+       @Test
+       fun `filter returns part of the string if start is set to index within string`() {
+           assertThat(filterText("start" to 13), equalTo<Any>("nopqrstuvwxyz"))
+       }
+
+       @Test
+       fun `filter returns last three characters if start is set to minus three`() {
+               assertThat(filterText("start" to -3), equalTo<Any>("xyz"))
+       }
+
+       @Test
+       fun `filter returns center part of string with start and length set`() {
+           assertThat(filterText("start" to 13, "length" to 3), equalTo<Any>("nop"))
+       }
+
+       @Test
+       fun `filter returns end part of string with start and too-large length set`() {
+           assertThat(filterText("start" to 23, "length" to 30), equalTo<Any>("xyz"))
+       }
+
+       @Test
+       fun `filter returns end part of string with negative start and too-large length set`() {
+           assertThat(filterText("start" to -3, "length" to 30), equalTo<Any>("xyz"))
+       }
+
+       @Test
+       fun `filter returns part of end of string with negative start and small length set`() {
+           assertThat(filterText("start" to -6, "length" to 3), equalTo<Any>("uvw"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/TrustAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/TrustAccessorTest.kt
new file mode 100644 (file)
index 0000000..9f2213a
--- /dev/null
@@ -0,0 +1,30 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.freenet.wot.Trust
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [TrustAccessor].
+ */
+class TrustAccessorTest {
+
+       private val accessor = TrustAccessor()
+
+       @Test
+       fun `accessor returns false if there is no explicit trust assigned`() {
+               assertThat(accessor.get(null, Trust(null, null, null), "assigned"), equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns true if there is explicit trust assigned`() {
+               assertThat(accessor.get(null, Trust(0, null, null), "assigned"), equalTo<Any>(true))
+       }
+
+       @Test
+       fun `reflection accessor is used for other members`() {
+               assertThat(accessor.get(null, Trust(0, 0, 0), "hashCode"), equalTo<Any>(Trust(0, 0, 0).hashCode()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/UniqueElementFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/UniqueElementFilterTest.kt
new file mode 100644 (file)
index 0000000..a7b21c1
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.template
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [UniqueElementFilter].
+ */
+class UniqueElementFilterTest {
+
+       private val filter = UniqueElementFilter()
+
+       @Test
+       fun `filter returns object if object is not a collection`() {
+               val someObject = Any()
+               assertThat(filter.format(null, someObject, null), equalTo<Any>(someObject))
+       }
+
+       @Test
+       fun `filter returns a set containing all unique elements of a given collection`() {
+               val objects = listOf(Any(), Any(), Any())
+               val collection = listOf(objects[0], objects[1], objects[2], objects[0], objects[1])
+               assertThat(filter.format(null, collection, null), equalTo<Any>(objects.toSet()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/UnknownDateFilterTest.kt
new file mode 100644 (file)
index 0000000..f18033e
--- /dev/null
@@ -0,0 +1,36 @@
+package net.pterodactylus.sone.template
+
+import freenet.l10n.BaseL10n
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [UnknownDateFilter].
+ */
+class UnknownDateFilterTest {
+
+       private val baseL10n = mock<BaseL10n>()
+       private val unknownKey = "unknown.key"
+       private val filter = UnknownDateFilter(baseL10n, unknownKey)
+
+       @Test
+       fun `filter returns given object for non-longs`() {
+           val someObject = Any()
+               assertThat(filter.format(null, someObject, null), equalTo<Any>(someObject))
+       }
+
+       @Test
+       fun `filter returns translated value of unknown key if zero is given`() {
+           whenever(baseL10n.getString(unknownKey)).thenReturn("translated")
+               assertThat(filter.format(null, 0L, null), equalTo<Any>("translated"))
+       }
+
+       @Test
+       fun `filter returns original long if non-zero value is given`() {
+               assertThat(filter.format(null, 1L, null), equalTo<Any>(1L))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt b/src/test/kotlin/net/pterodactylus/sone/test/Guice.kt
new file mode 100644 (file)
index 0000000..f5f52b8
--- /dev/null
@@ -0,0 +1,27 @@
+package net.pterodactylus.sone.test
+
+import com.google.inject.Injector
+import com.google.inject.Module
+import javax.inject.Provider
+import kotlin.reflect.KClass
+
+fun <T : Any> KClass<T>.isProvidedBy(instance: T) = Module { it.bind(this.java).toProvider { instance } }
+fun <T : Any> KClass<T>.isProvidedBy(provider: com.google.inject.Provider<T>) = Module { it.bind(this.java).toProvider(provider) }
+fun <T : Any> KClass<T>.isProvidedBy(provider: KClass<out Provider<T>>) = Module { it.bind(this.java).toProvider(provider.java) }
+inline fun <reified T : Any> KClass<T>.isProvidedByMock() = Module { it.bind(this.java).toProvider { mock<T>() } }
+
+inline fun <reified T : Any> Injector.getInstance() = getInstance(T::class.java)!!
+
+fun <T : Any> supply(javaClass: Class<T>): Source<T> = object : Source<T> {
+       override fun fromInstance(instance: T) = Module { it.bind(javaClass).toInstance(instance) }
+       override fun byInstance(instance: T) = Module { it.bind(javaClass).toProvider { instance } }
+       override fun byProvider(provider: com.google.inject.Provider<T>) = Module { it.bind(javaClass).toProvider(provider) }
+       override fun byProvider(provider: Class<Provider<T>>) = Module { it.bind(javaClass).toProvider(provider) }
+}
+
+interface Source<T : Any> {
+       fun fromInstance(instance: T): Module
+       fun byInstance(instance: T): Module
+       fun byProvider(provider: com.google.inject.Provider<T>): Module
+       fun byProvider(provider: Class<Provider<T>>): Module
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mockotlin.kt
new file mode 100644 (file)
index 0000000..c4fd7cb
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.test
+
+import com.google.inject.Module
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.OngoingStubbing
+
+inline fun <reified T : Any> mock(): T = Mockito.mock<T>(T::class.java)!!
+inline fun <reified T : Any> mockBuilder(): T = Mockito.mock<T>(T::class.java, Mockito.RETURNS_SELF)!!
+inline fun <reified T : Any> deepMock(): T = Mockito.mock<T>(T::class.java, Mockito.RETURNS_DEEP_STUBS)!!
+inline fun <reified T : Any> selfMock(): T = Mockito.mock<T>(T::class.java, Mockito.RETURNS_SELF)!!
+inline fun <reified T : Any> capture(): ArgumentCaptor<T> = ArgumentCaptor.forClass(T::class.java)
+
+inline fun <reified E: Throwable> OngoingStubbing<*>.doThrow(): OngoingStubbing<*> = thenThrow(E::class.java)
+
+inline fun <reified T : Any> bind(implementation: T): Module =
+               Module { it!!.bind(T::class.java).toInstance(implementation) }
+
+inline fun <reified T : Any> bindMock(): Module =
+               Module { it!!.bind(T::class.java).toInstance(mock<T>()) }
+
+inline fun <reified T: Any?> whenever(methodCall: T) = Mockito.`when`(methodCall)!!
+
+inline fun <reified T : Any> OngoingStubbing<T>.thenReturnMock(): OngoingStubbing<T> = this.thenReturn(mock<T>())
+
+operator fun <T> InvocationOnMock.get(index: Int): T = getArgument(index)
+
+inline fun <reified T> argumentCaptor(): ArgumentCaptor<T> = ArgumentCaptor.forClass<T, T>(T::class.java)!!
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/OneByOneMatcher.kt b/src/test/kotlin/net/pterodactylus/sone/test/OneByOneMatcher.kt
new file mode 100644 (file)
index 0000000..f0cceca
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.test
+
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeDiagnosingMatcher
+
+class OneByOneMatcher<A> : TypeSafeDiagnosingMatcher<A>() {
+       private data class Matcher<in A, out V>(val expected: V, val actual: (A) -> V, val description: String)
+
+       private val matchers = mutableListOf<Matcher<A, *>>()
+
+       fun <V> expect(description: String, expected: V, actual: (A) -> V) {
+               matchers += Matcher<A, V>(expected, actual, description)
+       }
+
+       override fun describeTo(description: Description) {
+               matchers.forEachIndexed { index, matcher ->
+                       if (index > 0) {
+                               description.appendText(", ")
+                       }
+                       description.appendText("${matcher.description} is ").appendValue(matcher.expected)
+               }
+       }
+
+       override fun matchesSafely(item: A, mismatchDescription: Description) =
+                       matchers.all {
+                               if (it.expected != it.actual(item)) {
+                                       mismatchDescription.appendText("${it.description} is ").appendValue(it.actual(item))
+                                       false
+                               } else {
+                                       true
+                               }
+                       }
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/PaginationMatcher.kt b/src/test/kotlin/net/pterodactylus/sone/test/PaginationMatcher.kt
new file mode 100644 (file)
index 0000000..5a47ca7
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.sone.utils.Pagination
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeDiagnosingMatcher
+
+/**
+ * Hamcrest matcher for [Pagination]s.
+ */
+class PaginationMatcher(private val page: Int? = null, private val pages: Int? = null):
+               TypeSafeDiagnosingMatcher<Pagination<*>>() {
+
+       override fun matchesSafely(pagination: Pagination<*>, mismatchDescription: Description): Boolean {
+               page?.let {
+                       if (pagination.page != page) {
+                               mismatchDescription.appendText("page is ").appendValue(pagination.page)
+                               return false
+                       }
+               }
+               pages?.let {
+                       if (pagination.pageCount != pages) {
+                               mismatchDescription.appendText("total pages is ").appendValue(pagination.pageCount)
+                               return false
+                       }
+               }
+               return true
+       }
+
+       override fun describeTo(description: Description) {
+               page?.also {
+                       description.appendText("is on page ").appendValue(page)
+                       pages?.also {
+                                       description.appendText(" of ").appendValue(pages)
+                       }
+               } ?: pages?.also {
+                       description.appendText("has ").appendValue(pages).appendText(" pages")
+               }
+       }
+
+       fun isOnPage(page: Int) = PaginationMatcher(page = page, pages = pages)
+       fun hasPages(pages: Int) = PaginationMatcher(page = page, pages = pages)
+
+}
+
+fun isOnPage(page: Int) = PaginationMatcher(page = page)
+fun hasPages(pages: Int) = PaginationMatcher(pages = pages)
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/FreenetLinkPartTest.kt
new file mode 100644 (file)
index 0000000..fcf6e67
--- /dev/null
@@ -0,0 +1,17 @@
+package net.pterodactylus.sone.text
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.junit.Test
+
+/**
+ * Unit test for [FreenetLinkPart].
+ */
+class FreenetLinkPartTest {
+
+       @Test
+       fun linkIsUsedAsTitleIfNoTextIsGiven() {
+               assertThat(FreenetLinkPart("link", "text", true).title, `is`("link"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/LinkPartTest.kt
new file mode 100644 (file)
index 0000000..373a848
--- /dev/null
@@ -0,0 +1,17 @@
+package net.pterodactylus.sone.text
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.junit.Test
+
+/**
+ * Unit test for [LinkPart].
+ */
+class LinkPartTest {
+
+       @Test
+       fun linkIsUsedAsTitleIfNoTitleIsGiven() {
+               assertThat(LinkPart("link", "text").title, `is`("link"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SonePartTest.kt
new file mode 100644 (file)
index 0000000..361bd19
--- /dev/null
@@ -0,0 +1,30 @@
+package net.pterodactylus.sone.text
+
+import net.pterodactylus.sone.data.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+/**
+ * Unit test for [SonePart].
+ */
+class SonePartTest {
+
+       private val sone = mock<Sone>()
+
+       init {
+               `when`(sone.profile).thenReturn(mock<Profile>())
+               `when`(sone.name).thenReturn("sone")
+       }
+
+       private val part = SonePart(sone)
+
+       @Test
+       fun textIsConstructedFromSonesNiceName() {
+               assertThat<String>(part.text, `is`<String>("sone"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/text/TimeTextConverterTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/TimeTextConverterTest.kt
new file mode 100644 (file)
index 0000000..cd8b3ed
--- /dev/null
@@ -0,0 +1,122 @@
+package net.pterodactylus.sone.text
+
+import net.pterodactylus.sone.freenet.L10nText
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import java.util.concurrent.TimeUnit.DAYS
+import java.util.concurrent.TimeUnit.HOURS
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+/**
+ * Unit test for [TimeTextConverter].
+ */
+class TimeTextConverterTest {
+
+       val now = System.currentTimeMillis()
+       val converter = TimeTextConverter { now }
+
+       private fun verifyInterval(startTime: Long, end: Long, vararg expectedTimeTexts: TimeText) {
+               assertThat(converter.getTimeText(now - startTime), equalTo(expectedTimeTexts[0]))
+               assertThat(converter.getTimeText(now - (end - 1)), equalTo(expectedTimeTexts[1 % expectedTimeTexts.size]))
+       }
+
+       @Test
+       fun `time of zero returns the l10n key for "unknown" and a refresh time of 12 hours`() {
+               assertThat(converter.getTimeText(0), equalTo(TimeText(L10nText("View.Sone.Text.UnknownDate"), HOURS.toMillis(12))))
+       }
+
+       @Test
+       fun `time in the future returns the correct l10n key and refresh of 5 minutes`() {
+               assertThat(converter.getTimeText(now + 1), equalTo(TimeText(L10nText("View.Time.InTheFuture"), MINUTES.toMillis(5))))
+       }
+
+       @Test
+       fun `time of zero to twenty seconds ago returns l10n key for "a few seconds ago" and refresh of 10 seconds`() {
+               verifyInterval(0, SECONDS.toMillis(20), TimeText(L10nText("View.Time.AFewSecondsAgo"), SECONDS.toMillis(10)))
+       }
+
+       @Test
+       fun `time of twenty to forty-five seconds ago returns l10n key for "half a minute ago" and refresh of 20 seconds`() {
+               verifyInterval(SECONDS.toMillis(20), SECONDS.toMillis(45), TimeText(L10nText("View.Time.HalfAMinuteAgo"), SECONDS.toMillis(20)))
+       }
+
+       @Test
+       fun `time of forty-five to ninety seconds ago returns l10n key for "a minute ago" and a refresh time of 1 minute`() {
+               verifyInterval(SECONDS.toMillis(45), SECONDS.toMillis(90), TimeText(L10nText("View.Time.AMinuteAgo"), MINUTES.toMillis(1)))
+       }
+
+       @Test
+       fun `time of ninety seconds to thirty minutes ago returns l10n key for "x minutes ago," the number of minutes, and a refresh time of 1 minute`() {
+               verifyInterval(SECONDS.toMillis(90), MINUTES.toMillis(30),
+                               TimeText(L10nText("View.Time.XMinutesAgo", listOf(2L)), MINUTES.toMillis(1)),
+                               TimeText(L10nText("View.Time.XMinutesAgo", listOf(30L)), MINUTES.toMillis(1)))
+       }
+
+       @Test
+       fun `time of thirty to forty-five minutes ago returns l10n key for "half an hour ago" and a refresh time of 10 minutes`() {
+               verifyInterval(MINUTES.toMillis(30), MINUTES.toMillis(45), TimeText(L10nText("View.Time.HalfAnHourAgo"), MINUTES.toMillis(10)))
+       }
+
+       @Test
+       fun `time of forty-five to ninety minutes ago returns l10n key for "an hour ago" and a refresh time of 1 hour`() {
+               verifyInterval(MINUTES.toMillis(45), MINUTES.toMillis(90), TimeText(L10nText("View.Time.AnHourAgo"), HOURS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of ninety minutes to twenty-one hours ago returns l10n key for "x hours ago," the number of hours, and a refresh time of 1 hour`() {
+               verifyInterval(MINUTES.toMillis(90), HOURS.toMillis(21),
+                               TimeText(L10nText("View.Time.XHoursAgo", listOf(2L)), HOURS.toMillis(1)),
+                               TimeText(L10nText("View.Time.XHoursAgo", listOf(21L)), HOURS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of twenty-one to forty-two hours ago returns l10n key for "a day ago" and a refresh time of 1 day`() {
+               verifyInterval(HOURS.toMillis(21), HOURS.toMillis(42), TimeText(L10nText("View.Time.ADayAgo"), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of forty-two hours to six days ago returns l10n key for "x days ago," the number of days, and a refresh time of 1 day`() {
+               verifyInterval(HOURS.toMillis(42), DAYS.toMillis(6),
+                               TimeText(L10nText("View.Time.XDaysAgo", listOf(2L)), DAYS.toMillis(1)),
+                               TimeText(L10nText("View.Time.XDaysAgo", listOf(6L)), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of six to eleven days ago returns l10n key for "a week ago" and a refresh time of 1 day`() {
+               verifyInterval(DAYS.toMillis(6), DAYS.toMillis(11), TimeText(L10nText("View.Time.AWeekAgo"), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of eleven to twenty-eight days ago returns l10n key for "x weeks ago," the number of weeks, and a refresh time of 1 day`() {
+               verifyInterval(DAYS.toMillis(11), DAYS.toMillis(28),
+                               TimeText(L10nText("View.Time.XWeeksAgo", listOf(2L)), DAYS.toMillis(1)),
+                               TimeText(L10nText("View.Time.XWeeksAgo", listOf(4L)), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of twenty-eight to forty-two days ago returns l10n key for "a month ago" and a refresh time of 1 day`() {
+               verifyInterval(DAYS.toMillis(28), DAYS.toMillis(42), TimeText(L10nText("View.Time.AMonthAgo"), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of forty-two to three hundred and thirty days ago returns l10n key for "x months ago," the number of months, and a refresh time of 1 day`() {
+               verifyInterval(DAYS.toMillis(42), DAYS.toMillis(330),
+                               TimeText(L10nText("View.Time.XMonthsAgo", listOf(1L)), DAYS.toMillis(1)),
+                               TimeText(L10nText("View.Time.XMonthsAgo", listOf(11L)), DAYS.toMillis(1)))
+       }
+
+       @Test
+       fun `time of three hundred and thirty to five hundred and forty days days ago returns l10n key for "a year ago" and a refresh time of 7 days`() {
+               verifyInterval(DAYS.toMillis(330), DAYS.toMillis(540), TimeText(L10nText("View.Time.AYearAgo"), DAYS.toMillis(7)))
+       }
+
+       @Test
+       fun `time of five hunder and forty to infinity days ago returns l10n key for "x years ago," the number of years, and a refresh time of 7 days`() {
+               verifyInterval(DAYS.toMillis(540), DAYS.toMillis(6000),
+                               TimeText(L10nText("View.Time.XYearsAgo", listOf(1L)), DAYS.toMillis(7)),
+                               TimeText(L10nText("View.Time.XYearsAgo", listOf(16L)), DAYS.toMillis(7)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/AutoCloseableBucketTest.kt
new file mode 100644 (file)
index 0000000..a705844
--- /dev/null
@@ -0,0 +1,26 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.Bucket
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class AutoCloseableBucketTest {
+
+       private val bucket = mock<Bucket>()
+       private val autoCloseableBucket = AutoCloseableBucket(bucket)
+
+       @Test
+       fun `bucket can be retrieved`() {
+               assertThat(autoCloseableBucket.bucket, equalTo(bucket))
+       }
+
+       @Test
+       fun `bucket will be free’d when close is called`() {
+               autoCloseableBucket.close()
+               verify(bucket).free()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/BooleansTest.kt
new file mode 100644 (file)
index 0000000..56627c3
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.utils
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [Booleans].
+ */
+class BooleansTest {
+
+       @Test
+       fun `ifTrue is executed if boolean is true`() {
+               assertThat(true.ifTrue { true }, equalTo(true))
+       }
+
+       @Test
+       fun `ifTrue is not executed if boolean is false`() {
+               assertThat(false.ifTrue { true }, nullValue())
+       }
+
+       @Test
+       fun `ifFalse is executed if boolean is false`() {
+               assertThat(false.ifFalse { true }, equalTo(true))
+       }
+
+       @Test
+       fun `ifFalse is not executed if boolean is true`() {
+               assertThat(true.ifFalse { true }, nullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/BucketsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/BucketsTest.kt
new file mode 100644 (file)
index 0000000..a17bdd2
--- /dev/null
@@ -0,0 +1,34 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.Bucket
+import net.pterodactylus.sone.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+import kotlin.test.fail
+
+/**
+ * Unit test for [freenet.support.api.Bucket]-related utilities.
+ */
+class BucketsTest {
+
+       private val bucket = mock<Bucket>()
+
+       @Test
+       fun `bucket is freed after use without exception`() {
+               bucket.use { }
+               verify(bucket).free()
+       }
+
+       @Test
+       fun `bucket is freed after use with exceptions`() {
+               try {
+                       bucket.use { throw Exception() }
+                       @Suppress("UNREACHABLE_CODE")
+                       fail()
+               } catch (e: Exception) {
+               } finally {
+                       verify(bucket).free()
+               }
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/JsonTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/JsonTest.kt
new file mode 100644 (file)
index 0000000..9c68e62
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.utils
+
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.instanceOf
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for JSON utilities.
+ */
+class JsonTest {
+
+       @Test
+       fun `object node is created correctly`() {
+               val objectNode = jsonObject {
+                       put("foo", "bar")
+               }
+               assertThat(objectNode, instanceOf(ObjectNode::class.java))
+               assertThat(objectNode.toString(), equalTo("{\"foo\":\"bar\"}"))
+       }
+
+       @Test
+       fun `object node is created with correctly-typed properties`() {
+           val objectNode = jsonObject("string" to "foo", "int" to 1, "long" to 2L, "boolean" to true, "other" to Any())
+               assertThat(objectNode["string"].isTextual, equalTo(true))
+               assertThat(objectNode["string"].asText(), equalTo("foo"))
+               assertThat(objectNode["int"].isInt, equalTo(true))
+               assertThat(objectNode["int"].asInt(), equalTo(1))
+               assertThat(objectNode["long"].isLong, equalTo(true))
+               assertThat(objectNode["long"].asLong(), equalTo(2L))
+               assertThat(objectNode["boolean"].isBoolean, equalTo(true))
+               assertThat(objectNode["boolean"].asBoolean(), equalTo(true))
+               assertThat(objectNode["other"], nullValue())
+       }
+
+       @Test
+       fun `array node is created correctly`() {
+               val arrayNode = listOf(
+                               jsonObject { put("foo", "bar") },
+                               jsonObject { put("baz", "quo") }
+               ).toArray()
+               assertThat(arrayNode, instanceOf(ArrayNode::class.java))
+               assertThat(arrayNode.toString(), equalTo("[{\"foo\":\"bar\"},{\"baz\":\"quo\"}]"))
+       }
+
+       @Test
+       fun `array is created correctly for strings`() {
+           val arrayNode = jsonArray("foo", "bar", "baz")
+               assertThat(arrayNode.toString(), equalTo("[\"foo\",\"bar\",\"baz\"]"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/ObjectsTest.kt
new file mode 100644 (file)
index 0000000..1c2d7f5
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.utils
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.empty
+import org.junit.Test
+
+/**
+ * Unit test for Object utils.
+ */
+class ObjectsTest {
+
+       @Test
+       fun `non-null value is turned into a list with one element`() {
+               assertThat(5.asList(), contains(5))
+       }
+
+       @Test
+       fun `null value is turned into empty list`() {
+               assertThat(null.asList(), empty())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/OptionalsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/OptionalsTest.kt
new file mode 100644 (file)
index 0000000..76dd443
--- /dev/null
@@ -0,0 +1,61 @@
+package net.pterodactylus.sone.utils
+
+import com.google.common.base.Optional
+import com.google.common.base.Optional.fromNullable
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Test for [Optional] utils.
+ */
+class OptionalsTest {
+
+       @Test
+       fun `present optional can be transformed with let`() {
+               val optional = Optional.of(1)
+               assertThat(optional.let { it + 1 }, equalTo(2))
+       }
+
+       @Test
+       fun `empty optional is transform to null with let`() {
+               val optional = Optional.absent<Int>()
+               assertThat(optional.let { it + 1 }, nullValue())
+       }
+
+       @Test
+       fun `present optional can be processed with also`() {
+               val called = AtomicBoolean(false)
+               Optional.of(1).also { if (it == 1) called.set(true) }
+               assertThat(called.get(), equalTo(true))
+       }
+
+       @Test
+       fun `absent optional is not processed with also`() {
+               val called = AtomicBoolean(false)
+               Optional.absent<Int>().also { called.set(true) }
+               assertThat(called.get(), equalTo(false))
+       }
+
+       @Test
+       fun `1 as optional is correct optional`() {
+               val optional = 1.asOptional()
+               assertThat(optional.get(), equalTo(1))
+       }
+
+       @Test
+       fun `null as optional is asent optional`() {
+               val optional = null.asOptional()
+               assertThat(optional.isPresent, equalTo(false))
+       }
+
+       @Test
+       fun testMapPresent() {
+               val originalList = listOf(1, 2, null, 3, null)
+               assertThat(originalList.mapPresent { fromNullable(it) }, contains(1, 2, 3))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/PaginationTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/PaginationTest.kt
new file mode 100644 (file)
index 0000000..3ce20ce
--- /dev/null
@@ -0,0 +1,187 @@
+package net.pterodactylus.sone.utils
+
+import net.pterodactylus.sone.test.hasPages
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [Pagination].
+ */
+class PaginationTest {
+
+       private val items = listOf(1, 2, 3, 4, 5)
+       private val pagination = Pagination<Int>(items, 2)
+
+       @Test
+       fun `pagination can be created from iterable`() {
+               val pagination = listOf(1, 2, 3, 4, 5).asIterable().paginate(2)
+               assertThat(pagination, hasPages(3).isOnPage(0))
+       }
+
+       @Test
+       fun `new pagination is at page 0`() {
+               assertThat(pagination.page, equalTo(0))
+       }
+
+       @Test
+       fun `new pagination is at page number 1`() {
+               assertThat(pagination.pageNumber, equalTo(1))
+       }
+
+       @Test
+       fun `setting a page to less than 0 sets page to 0`() {
+               pagination.page = -1
+               assertThat(pagination.page, equalTo(0))
+       }
+
+       @Test
+       fun `setting page to a valid page sets page`() {
+               pagination.page = 1
+               assertThat(pagination.page, equalTo(1))
+       }
+
+       @Test
+       fun `setting a too large page will cap the page`() {
+               pagination.page = 100
+               assertThat(pagination.page, equalTo(2))
+       }
+
+       @Test
+       fun `the page count is returned correctly`() {
+               assertThat(pagination.pageCount, equalTo(3))
+       }
+
+       @Test
+       fun `page size is returned correctly`() {
+               assertThat(pagination.pageSize, equalTo(2))
+       }
+
+       @Test
+       fun `a page size of less than 1 is set to 1`() {
+               pagination.pageSize = 0
+               assertThat(pagination.pageSize, equalTo(1))
+       }
+
+       @Test
+       fun `changing page size sets new page correctly`() {
+               pagination.page = 1
+               pagination.pageSize = 1
+               assertThat(pagination.page, equalTo(2))
+       }
+
+       @Test
+       fun `changing page size to very large returns to page 0`() {
+               pagination.page = 1
+               pagination.pageSize = 20
+               assertThat(pagination.page, equalTo(0))
+       }
+
+       @Test
+       fun `item count for current page is page size of first page`() {
+               assertThat(pagination.itemCount, equalTo(2))
+       }
+
+       @Test
+       fun `item count for last page is 1`() {
+               pagination.page = 2
+               assertThat(pagination.itemCount, equalTo(1))
+       }
+
+       @Test
+       fun `items on first page are returned correctly`() {
+               assertThat(pagination.items, contains(1, 2))
+       }
+
+       @Test
+       fun `items on last page are returned correctly`() {
+               pagination.page = 2
+               assertThat(pagination.items, contains(5))
+       }
+
+       @Test
+       fun `pagination is first on first page`() {
+               assertThat(pagination.isFirst, equalTo(true))
+       }
+
+       @Test
+       fun `pagination is not first on second page`() {
+               pagination.page = 1
+               assertThat(pagination.isFirst, equalTo(false))
+       }
+
+       @Test
+       fun `pagination is not first on last page`() {
+               pagination.page = 2
+               assertThat(pagination.isFirst, equalTo(false))
+       }
+
+       @Test
+       fun `pagination is not last  on first page`() {
+               assertThat(pagination.isLast, equalTo(false))
+       }
+
+       @Test
+       fun `pagination is not last on second page`() {
+               pagination.page = 1
+               assertThat(pagination.isLast, equalTo(false))
+       }
+
+       @Test
+       fun `pagination is last on last page`() {
+               pagination.page = 2
+               assertThat(pagination.isLast, equalTo(true))
+       }
+
+       @Test
+       fun `pagination is necessary for three pages`() {
+               assertThat(pagination.isNecessary, equalTo(true))
+       }
+
+       @Test
+       fun `pagination is necessary for two pages`() {
+               pagination.pageSize = 4
+               assertThat(pagination.isNecessary, equalTo(true))
+       }
+
+       @Test
+       fun `pagination is not necessary for one page`() {
+               pagination.pageSize = 20
+               assertThat(pagination.isNecessary, equalTo(false))
+       }
+
+       @Test
+       fun `previous page is returned correctly for second page`() {
+               pagination.page = 1
+               assertThat(pagination.previousPage, equalTo(0))
+       }
+
+       @Test
+       fun `previous page is returned correctly for last page`() {
+               pagination.page = 2
+               assertThat(pagination.previousPage, equalTo(1))
+       }
+
+       @Test
+       fun `next page is returned correctly for first page`() {
+               assertThat(pagination.nextPage, equalTo(1))
+       }
+
+       @Test
+       fun `next page is returned correctly for second page`() {
+               pagination.page = 1
+               assertThat(pagination.nextPage, equalTo(2))
+       }
+
+       @Test
+       fun `last page is returned correctly`() {
+               assertThat(pagination.lastPage, equalTo(2))
+       }
+
+       @Test
+       fun `iterator returns items on the current page`() {
+               assertThat(pagination.iterator().asSequence().toList(), contains(1, 2))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/RequestsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/RequestsTest.kt
new file mode 100644 (file)
index 0000000..f2a97f0
--- /dev/null
@@ -0,0 +1,121 @@
+package net.pterodactylus.sone.utils
+
+import freenet.support.api.HTTPRequest
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import net.pterodactylus.util.web.Request
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+
+/**
+ * Unit test for the [Request] utilities.
+ */
+class RequestsTest {
+
+       private val httpGetRequest = mock<HTTPRequest>().apply { whenever(method).thenReturn("GET") }
+       private val httpPostRequest = mock<HTTPRequest>().apply { whenever(method).thenReturn("POST") }
+       private val getRequest = mock<Request>().apply { whenever(method).thenReturn(GET) }
+       private val postRequest = mock<Request>().apply { whenever(method).thenReturn(POST) }
+       private val freenetGetRequest = mock<FreenetRequest>().apply {
+               whenever(method).thenReturn(GET)
+               whenever(httpRequest).thenReturn(this@RequestsTest.httpGetRequest)
+       }
+       private val freenetPostRequest = mock<FreenetRequest>().apply {
+               whenever(method).thenReturn(POST)
+               whenever(httpRequest).thenReturn(this@RequestsTest.httpPostRequest)
+       }
+
+       @Test
+       fun `GET request is recognized correctly`() {
+               assertThat(getRequest.isGET, equalTo(true))
+               assertThat(getRequest.isPOST, equalTo(false))
+       }
+
+       @Test
+       fun `POST request is recognized correctly`() {
+               assertThat(postRequest.isGET, equalTo(false))
+               assertThat(postRequest.isPOST, equalTo(true))
+       }
+
+       @Test
+       fun `correct parameter of GET request is returned`() {
+               whenever(httpGetRequest.getParam("test-param")).thenAnswer { "test-value" }
+               assertThat(freenetGetRequest.parameters["test-param"], equalTo("test-value"))
+       }
+
+       @Test
+       fun `correct parameter of POST request is returned`() {
+               whenever(httpPostRequest.getPartAsStringFailsafe(eq("test-param"), anyInt())).thenAnswer { "test-value" }
+               assertThat(freenetPostRequest.parameters["test-param"], equalTo("test-value"))
+       }
+
+       @Test
+       fun `parameter of unknown request is not returned`() {
+               val request = mock<FreenetRequest>()
+               val httpRequest = mock<HTTPRequest>()
+               whenever(request.httpRequest).thenReturn(httpRequest)
+               assertThat(request.parameters["test-param"], nullValue())
+       }
+
+       @Test
+       fun `parameter of GET request is checked for presence correctly`() {
+               whenever(httpGetRequest.isParameterSet("test-param")).thenAnswer { true }
+               assertThat("test-param" in freenetGetRequest.parameters, equalTo(true))
+       }
+
+       @Test
+       fun `parameter of POST request is checked for presence correctly`() {
+               whenever(httpPostRequest.isPartSet("test-param")).thenAnswer { true }
+               assertThat("test-param" in freenetPostRequest.parameters, equalTo(true))
+       }
+
+       @Test
+       fun `parameter of unknown request is not set`() {
+               val request = mock<FreenetRequest>()
+               val httpRequest = mock<HTTPRequest>()
+               whenever(request.httpRequest).thenReturn(httpRequest)
+               assertThat("test-param" in request.parameters, equalTo(false))
+       }
+
+       @Test
+       fun `http header of freenet request can be retrieved`() {
+               whenever(httpGetRequest.getHeader("foo")).thenReturn("bar")
+               assertThat(freenetGetRequest.headers["foo"], equalTo("bar"))
+       }
+
+       @Test
+       fun `http header of freenet request is case-insensitive`() {
+               whenever(httpGetRequest.getHeader("foo")).thenReturn("bar")
+               assertThat(freenetGetRequest.headers["FOO"], equalTo("bar"))
+       }
+
+       @Test
+       fun `value of non-existant http header of freenet request is null`() {
+               assertThat(freenetGetRequest.headers["Foo"], nullValue())
+       }
+
+       @Test
+       fun `http header of http request can be retrieved`() {
+               whenever(httpGetRequest.getHeader("foo")).thenReturn("bar")
+               assertThat(httpGetRequest.headers["foo"], equalTo("bar"))
+       }
+
+       @Test
+       fun `http header of http request is case-insensitive`() {
+               whenever(httpGetRequest.getHeader("foo")).thenReturn("bar")
+               assertThat(httpGetRequest.headers["FOO"], equalTo("bar"))
+       }
+
+       @Test
+       fun `value of non-existant http header of http request is null`() {
+               assertThat(httpGetRequest.headers["Foo"], nullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/StringsTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/StringsTest.kt
new file mode 100644 (file)
index 0000000..e567c0f
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.utils
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [StringsKt].
+ */
+class StringsTest {
+
+       @Test
+       fun `non-empty string is returned as-is`() {
+               assertThat("non-empty".emptyToNull, equalTo("non-empty"))
+       }
+
+       @Test
+       fun `string with whitespace only is returned as null`() {
+               assertThat("   ".emptyToNull, nullValue())
+       }
+
+       @Test
+       fun `zero-length string is returned as null`() {
+               assertThat("".emptyToNull, nullValue())
+       }
+
+       @Test
+       fun `null is returned as null`() {
+               assertThat(null.emptyToNull, nullValue())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/BookmarkAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..17c334e
--- /dev/null
@@ -0,0 +1,42 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.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 [BookmarkAjaxPage].
+ */
+class BookmarkAjaxPageTest : JsonPageTest("bookmark.ajax", requiresLogin = false, pageSupplier = ::BookmarkAjaxPage) {
+
+       @Test
+       fun `missing post ID results in invalid id response`() {
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `empty post ID results in invalid id response`() {
+               addRequestParameter("post", "")
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `invalid post ID results in success but does not bookmark anything`() {
+               addRequestParameter("post", "missing")
+               assertThatJsonIsSuccessful()
+               verify(core, never()).bookmarkPost(any<Post>())
+       }
+
+       @Test
+       fun `valid post ID results in success and bookmarks the post`() {
+               addRequestParameter("post", "valid-post-id")
+               val post = addNewPost("valid-post-id", "1", 2)
+               assertThatJsonIsSuccessful()
+               verify(core).bookmarkPost(post)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreatePostAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..8449453
--- /dev/null
@@ -0,0 +1,92 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.google.common.base.Optional
+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.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+
+/**
+ * Unit test for [CreatePostAjaxPage].
+ */
+class CreatePostAjaxPageTest : JsonPageTest("createPost.ajax", pageSupplier = ::CreatePostAjaxPage) {
+
+       @Test
+       fun `missing text parameter returns error`() {
+               assertThatJsonFailed("text-required")
+       }
+
+       @Test
+       fun `empty text returns error`() {
+               addRequestParameter("text", "")
+               assertThatJsonFailed("text-required")
+       }
+
+       @Test
+       fun `whitespace-only text returns error`() {
+               addRequestParameter("text", "  ")
+               assertThatJsonFailed("text-required")
+       }
+
+       @Test
+       fun `request with valid data creates post`() {
+               addRequestParameter("text", "test")
+               val post = createPost()
+               whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post)
+               assertThatJsonIsSuccessful()
+               assertThat(json["postId"]?.asText(), equalTo("id"))
+               assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
+               assertThat(json["recipient"], nullValue())
+       }
+
+       @Test
+       fun `request with invalid recipient creates post without recipient`() {
+               addRequestParameter("text", "test")
+               addRequestParameter("recipient", "invalid")
+               val post = createPost()
+               whenever(core.createPost(currentSone, Optional.absent(), "test")).thenReturn(post)
+               assertThatJsonIsSuccessful()
+               assertThat(json["postId"]?.asText(), equalTo("id"))
+               assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
+               assertThat(json["recipient"], nullValue())
+       }
+
+       @Test
+       fun `request with valid data and recipient creates correct post`() {
+               addRequestParameter("text", "test")
+               addRequestParameter("recipient", "valid")
+               val recipient = mock<Sone>().apply { whenever(id).thenReturn("valid") }
+               addSone(recipient)
+               val post = createPost("valid")
+               whenever(core.createPost(currentSone, Optional.of(recipient), "test")).thenReturn(post)
+               assertThatJsonIsSuccessful()
+               assertThat(json["postId"]?.asText(), equalTo("id"))
+               assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
+               assertThat(json["recipient"]?.asText(), equalTo("valid"))
+       }
+
+       @Test
+       fun `text is filtered correctly`() {
+               addRequestParameter("text", "Link http://freenet.test:8888/KSK@foo is filtered")
+               addRequestHeader("Host", "freenet.test:8888")
+               val post = createPost()
+               whenever(core.createPost(currentSone, Optional.absent(), "Link KSK@foo is filtered")).thenReturn(post)
+               assertThatJsonIsSuccessful()
+               assertThat(json["postId"]?.asText(), equalTo("id"))
+               assertThat(json["sone"]?.asText(), equalTo(currentSone.id))
+               assertThat(json["recipient"], nullValue())
+       }
+
+       private fun createPost(recipientId: String? = null) =
+                       mock<Post>().apply {
+                               whenever(id).thenReturn("id")
+                               whenever(sone).thenReturn(currentSone)
+                               whenever(this.recipientId).thenReturn(recipientId.asOptional())
+                       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..829167e
--- /dev/null
@@ -0,0 +1,64 @@
+package net.pterodactylus.sone.web.ajax
+
+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.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [CreateReplyAjaxPage].
+ */
+class CreateReplyAjaxPageTest : JsonPageTest("createReply.ajax", pageSupplier = ::CreateReplyAjaxPage) {
+
+       @Test
+       fun `invalid post ID results in error message`() {
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `valid post ID results in created reply`() {
+               val post = mock<Post>()
+               addPost(post, "post-id")
+               val reply = mock<PostReply>().apply { whenever(id).thenReturn("reply-id") }
+               whenever(core.createReply(currentSone, post, "")).thenReturn(reply)
+           addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["reply"]?.asText(), equalTo("reply-id"))
+               assertThat(json["sone"]?.asText(), equalTo("soneId"))
+       }
+
+       @Test
+       fun `text is filtered when creating reply`() {
+               val post = mock<Post>()
+               addPost(post, "post-id")
+               val reply = mock<PostReply>().apply { whenever(id).thenReturn("reply-id") }
+               whenever(core.createReply(currentSone, post, "Text with KSK@foo.bar link")).thenReturn(reply)
+               addRequestParameter("post", "post-id")
+               addRequestParameter("text", "Text with http://127.0.0.1:8888/KSK@foo.bar link")
+               addRequestHeader("Host", "127.0.0.1:8888")
+               assertThatJsonIsSuccessful()
+               assertThat(json["reply"]?.asText(), equalTo("reply-id"))
+               assertThat(json["sone"]?.asText(), equalTo("soneId"))
+       }
+
+       @Test
+       fun `sender can be chosen from local sones`() {
+           val sone = mock<Sone>().apply { whenever(id).thenReturn("local-sone") }
+               addLocalSone(sone)
+               val post = mock<Post>()
+               addPost(post, "post-id")
+               val reply = mock<PostReply>().apply { whenever(id).thenReturn("reply-id") }
+               whenever(core.createReply(sone, post, "Text")).thenReturn(reply)
+               addRequestParameter("post", "post-id")
+               addRequestParameter("text", "Text")
+               addRequestParameter("sender", "local-sone")
+               assertThatJsonIsSuccessful()
+               assertThat(json["reply"]?.asText(), equalTo("reply-id"))
+               assertThat(json["sone"]?.asText(), equalTo("local-sone"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeletePostAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..cee50b9
--- /dev/null
@@ -0,0 +1,43 @@
+package net.pterodactylus.sone.web.ajax
+
+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 org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeletePostAjaxPage].
+ */
+class DeletePostAjaxPageTest : JsonPageTest("deletePost.ajax", pageSupplier = ::DeletePostAjaxPage) {
+
+       @Test
+       fun `missing post ID results in invalid id response`() {
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `post from non-local sone results in not authorized response`() {
+               val post = mock<Post>()
+               val sone = mock<Sone>()
+               whenever(post.sone).thenReturn(sone)
+               addPost(post, "post-id")
+               addRequestParameter("post", "post-id")
+               assertThatJsonFailed("not-authorized")
+       }
+
+       @Test
+       fun `post from local sone is deleted`() {
+               val post = mock<Post>()
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               whenever(post.sone).thenReturn(sone)
+               addPost(post, "post-id")
+               addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               verify(core).deletePost(post)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..330bedf
--- /dev/null
@@ -0,0 +1,30 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteProfileFieldAjaxPage].
+ */
+class DeleteProfileFieldAjaxPageTest : JsonPageTest("deleteProfileField.ajax", pageSupplier = ::DeleteProfileFieldAjaxPage) {
+
+       @Test
+       fun `request without field id results in invalid field id error`() {
+               assertThatJsonFailed("invalid-field-id")
+       }
+
+       @Test
+       fun `request with valid field id results in field deletion`() {
+               profile.addField("foo")
+               val fieldId = profile.getFieldByName("foo")!!.id
+               addRequestParameter("field", fieldId)
+               assertThatJsonIsSuccessful()
+               assertThat(profile.getFieldByName("foo"), nullValue())
+               verify(currentSone).profile = profile
+               verify(core).touchConfiguration()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..e39332c
--- /dev/null
@@ -0,0 +1,44 @@
+package net.pterodactylus.sone.web.ajax
+
+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 org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteReplyAjaxPage].
+ */
+class DeleteReplyAjaxPageTest : JsonPageTest("deleteReply.ajax", pageSupplier = ::DeleteReplyAjaxPage) {
+
+       @Test
+       fun `request with missing reply results in invalid id`() {
+               assertThatJsonFailed("invalid-reply-id")
+       }
+
+       @Test
+       fun `request with non-local reply id results in not authorized`() {
+               val reply = mock<PostReply>()
+               val sone = mock<Sone>()
+               whenever(reply.sone).thenReturn(sone)
+               addReply(reply, "reply-id")
+               addRequestParameter("reply", "reply-id")
+               assertThatJsonFailed("not-authorized")
+       }
+
+       @Test
+       fun `request with local reply id deletes reply`() {
+               val reply = mock<PostReply>()
+               val sone = mock<Sone>()
+               whenever(sone.isLocal).thenReturn(true)
+               whenever(reply.sone).thenReturn(sone)
+               addReply(reply, "reply-id")
+               addRequestParameter("reply", "reply-id")
+               assertThatJsonIsSuccessful()
+               verify(core).deleteReply(reply)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..ff603d2
--- /dev/null
@@ -0,0 +1,38 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.util.notify.Notification
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DismissNotificationAjaxPage].
+ */
+class DismissNotificationAjaxPageTest : JsonPageTest("dismissNotification.ajax", requiresLogin = false, pageSupplier = ::DismissNotificationAjaxPage) {
+
+       @Test
+       fun `request without notification returns invalid-notification-id`() {
+               assertThatJsonFailed("invalid-notification-id")
+       }
+
+       @Test
+       fun `request to dismiss non-dismissable notification results in not-dismissable`() {
+               val notification = mock<Notification>()
+               addNotification(notification, "foo")
+               addRequestParameter("notification", "foo")
+               assertThatJsonFailed("not-dismissable")
+       }
+
+       @Test
+       fun `request to dismiss dismissable notification dismisses notification`() {
+               val notification = mock<Notification>().apply { whenever(isDismissable).thenReturn(true) }
+               addNotification(notification, "foo")
+               addRequestParameter("notification", "foo")
+               assertThatJsonIsSuccessful()
+               verify(notification).dismiss()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/DistrustAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..78930e5
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DistrustAjaxPage].
+ */
+class DistrustAjaxPageTest : JsonPageTest("distrustSone.ajax", pageSupplier = ::DistrustAjaxPage) {
+
+       @Test
+       fun `request with missing sone results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with invalid sone results in invalid-sone-id`() {
+               addRequestParameter("sone", "invalid-sone")
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone results in distrusted sone`() {
+               val sone = mock<Sone>()
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).distrustSone(currentSone, sone)
+       }
+
+       @Test
+       fun `request with valid sone results in correct trust value being sent back`() {
+               core.preferences.negativeTrust = -33
+               val sone = mock<Sone>()
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["trustValue"]?.asInt(), equalTo(-33))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..098185b
--- /dev/null
@@ -0,0 +1,90 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Album.Modifier.AlbumTitleMustNotBeEmpty
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.impl.AlbumImpl
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [EditAlbumAjaxPage].
+ */
+class EditAlbumAjaxPageTest : JsonPageTest("editAlbum.ajax", pageSupplier = ::EditAlbumAjaxPage) {
+
+       private val sone = mock<Sone>()
+       private val localSone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+       private val album = mock<Album>().apply { whenever(id).thenReturn("album-id") }
+
+       @Test
+       fun `request without album results in invalid-album-id`() {
+               assertThatJsonFailed("invalid-album-id")
+       }
+
+       @Test
+       fun `request with non-local album results in not-authorized`() {
+               whenever(album.sone).thenReturn(sone)
+               addAlbum(album)
+               addRequestParameter("album", "album-id")
+               assertThatJsonFailed("not-authorized")
+       }
+
+       @Test
+       fun `request with moveLeft moves album to the left`() {
+               whenever(album.sone).thenReturn(localSone)
+               val swappedAlbum = mock<Album>().apply { whenever(id).thenReturn("swapped") }
+               val parentAlbum = mock<Album>()
+               whenever(parentAlbum.moveAlbumUp(album)).thenReturn(swappedAlbum)
+               whenever(album.parent).thenReturn(parentAlbum)
+               addAlbum(album)
+               addRequestParameter("album", "album-id")
+               addRequestParameter("moveLeft", "true")
+               assertThatJsonIsSuccessful()
+               assertThat(json["sourceAlbumId"]?.asText(), equalTo("album-id"))
+               assertThat(json["destinationAlbumId"]?.asText(), equalTo("swapped"))
+       }
+
+       @Test
+       fun `request with moveRight moves album to the right`() {
+               whenever(album.sone).thenReturn(localSone)
+               val swappedAlbum = mock<Album>().apply { whenever(id).thenReturn("swapped") }
+               val parentAlbum = mock<Album>()
+               whenever(parentAlbum.moveAlbumDown(album)).thenReturn(swappedAlbum)
+               whenever(album.parent).thenReturn(parentAlbum)
+               addAlbum(album)
+               addRequestParameter("album", "album-id")
+               addRequestParameter("moveRight", "true")
+               assertThatJsonIsSuccessful()
+               assertThat(json["sourceAlbumId"]?.asText(), equalTo("album-id"))
+               assertThat(json["destinationAlbumId"]?.asText(), equalTo("swapped"))
+       }
+
+       @Test
+       fun `request with missing title results in invalid-title`() {
+               whenever(album.sone).thenReturn(localSone)
+               whenever(album.modify()).thenReturn(deepMock())
+               whenever(album.modify().setTitle("")).thenThrow(AlbumTitleMustNotBeEmpty::class.java)
+               addAlbum(album)
+               addRequestParameter("album", "album-id")
+               assertThatJsonFailed("invalid-album-title")
+       }
+
+       @Test
+       fun `request with title and description sets title and filtered description`() {
+               val album = AlbumImpl(currentSone, "album-id")
+               addAlbum(album)
+               addRequestParameter("album", "album-id")
+               addRequestParameter("title", "new title")
+               addRequestParameter("description", "foo http://127.0.0.1:8888/KSK@foo.html link")
+               addRequestHeader("Host", "127.0.0.1:8888")
+               assertThatJsonIsSuccessful()
+               assertThat(json["albumId"]?.asText(), equalTo("album-id"))
+               assertThat(json["title"]?.asText(), equalTo("new title"))
+               assertThat(json["description"]?.asText(), equalTo("foo KSK@foo.html link"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditImageAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..dad6c02
--- /dev/null
@@ -0,0 +1,116 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Album
+import net.pterodactylus.sone.data.Image
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.impl.ImageImpl
+import net.pterodactylus.sone.template.ParserFilter
+import net.pterodactylus.sone.template.RenderFilter
+import net.pterodactylus.sone.template.ShortenFilter
+import net.pterodactylus.sone.test.argumentCaptor
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditImageAjaxPage].
+ */
+class EditImageAjaxPageTest : JsonPageTest("editImage.ajax") {
+
+       private val parserFilter = mock<ParserFilter>()
+       private val shortenFilter = mock<ShortenFilter>()
+       private val renderFilter = mock<RenderFilter>()
+       override val page: JsonPage get() = EditImageAjaxPage(webInterface, parserFilter, shortenFilter, renderFilter)
+
+       @Test
+       fun `request without image results in invalid-image-id`() {
+               assertThatJsonFailed("invalid-image-id")
+       }
+
+       @Test
+       fun `request with non-local image results in not-authorized`() {
+               val image = mock<Image>()
+               val sone = mock<Sone>()
+               whenever(image.sone).thenReturn(sone)
+               addImage(image, "image-id")
+               addRequestParameter("image", "image-id")
+               assertThatJsonFailed("not-authorized")
+       }
+
+       @Test
+       fun `moving an image to the left returns the correct values`() {
+               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               whenever(image.sone).thenReturn(sone)
+               val swapped = mock<Image>().apply { whenever(id).thenReturn("swapped") }
+               val album = mock<Album>()
+               whenever(album.moveImageUp(image)).thenReturn(swapped)
+               whenever(image.album).thenReturn(album)
+               addImage(image)
+               addRequestParameter("image", "image-id")
+               addRequestParameter("moveLeft", "true")
+               assertThatJsonIsSuccessful()
+               assertThat(json["sourceImageId"]?.asText(), equalTo("image-id"))
+               assertThat(json["destinationImageId"]?.asText(), equalTo("swapped"))
+               verify(core).touchConfiguration()
+       }
+
+       @Test
+       fun `moving an image to the right returns the correct values`() {
+               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               whenever(image.sone).thenReturn(sone)
+               val swapped = mock<Image>().apply { whenever(id).thenReturn("swapped") }
+               val album = mock<Album>()
+               whenever(album.moveImageDown(image)).thenReturn(swapped)
+               whenever(image.album).thenReturn(album)
+               addImage(image)
+               addRequestParameter("image", "image-id")
+               addRequestParameter("moveRight", "true")
+               assertThatJsonIsSuccessful()
+               assertThat(json["sourceImageId"]?.asText(), equalTo("image-id"))
+               assertThat(json["destinationImageId"]?.asText(), equalTo("swapped"))
+               verify(core).touchConfiguration()
+       }
+
+       @Test
+       fun `request with empty title results in invalid-image-title`() {
+               val image = mock<Image>().apply { whenever(id).thenReturn("image-id") }
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               whenever(image.sone).thenReturn(sone)
+               addImage(image)
+               addRequestParameter("image", "image-id")
+               assertThatJsonFailed("invalid-image-title")
+       }
+
+       @Test
+       fun `request with title and description returns correct values`() {
+               val sone = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+               val image = ImageImpl("image-id").modify().setSone(sone).update()
+               val parsed = Object()
+               val shortened = Object()
+               val rendered = "rendered description"
+               whenever(parserFilter.format(any(), eq("some KSK@foo link"), any())).thenReturn(parsed)
+               whenever(shortenFilter.format(any(), eq(parsed), any())).thenReturn(shortened)
+               whenever(renderFilter.format(any(), eq(shortened), any())).thenReturn(rendered)
+               addImage(image)
+               addRequestParameter("image", "image-id")
+               addRequestParameter("title", "some title")
+               addRequestParameter("description", "some http://127.0.0.1:8888/KSK@foo link")
+               addRequestHeader("Host", "127.0.0.1:8888")
+               assertThatJsonIsSuccessful()
+               assertThat(json["title"]?.asText(), equalTo("some title"))
+               assertThat(json["description"]?.asText(), equalTo("some KSK@foo link"))
+               assertThat(json["parsedDescription"]?.asText(), equalTo("rendered description"))
+               verify(core).touchConfiguration()
+               val parameterCaptor = argumentCaptor<MutableMap<String, Any?>>()
+               verify(parserFilter).format(any(), any(), parameterCaptor.capture())
+               assertThat(parameterCaptor.value["sone"], equalTo<Any>(sone))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..7eed9ac
--- /dev/null
@@ -0,0 +1,46 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditProfileFieldAjaxPage].
+ */
+class EditProfileFieldAjaxPageTest : JsonPageTest("editProfileField.ajax", pageSupplier = ::EditProfileFieldAjaxPage) {
+
+       @Test
+       fun `request without field id results in invalid-field-id`() {
+               assertThatJsonFailed("invalid-field-id")
+       }
+
+       @Test
+       fun `request with empty new name results in invalid-parameter-name`() {
+               val field = currentSone.profile.addField("test-field")
+               addRequestParameter("field", field.id)
+               addRequestParameter("name", "  \t ")
+               assertThatJsonFailed("invalid-parameter-name")
+       }
+
+       @Test
+       fun `request with duplicate new name results in duplicate-field-name`() {
+               currentSone.profile.addField("other-field")
+               val field = currentSone.profile.addField("test-field")
+               addRequestParameter("field", field.id)
+               addRequestParameter("name", "other-field")
+               assertThatJsonFailed("duplicate-field-name")
+       }
+
+       @Test
+       fun `request with valid field name changes field name`() {
+               val profile = currentSone.profile
+               val field = profile.addField("test-field")
+               addRequestParameter("field", field.id)
+               addRequestParameter("name", "  new name ")
+               assertThatJsonIsSuccessful()
+               assertThat(field.name, equalTo("new name"))
+               verify(currentSone).profile = profile
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..c649e75
--- /dev/null
@@ -0,0 +1,38 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+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 [FollowSoneAjaxPage].
+ */
+class FollowSoneAjaxPageTest : JsonPageTest("followSone.ajax", pageSupplier = ::FollowSoneAjaxPage) {
+
+       @Test
+       fun `request without sone id results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with sone follows sone`() {
+               addSone(mock<Sone>().apply { whenever(id).thenReturn("sone-id") })
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).followSone(currentSone, "sone-id")
+       }
+
+       @Test
+       fun `request with sone makes sone as known`() {
+               val sone = mock<Sone>()
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).markSoneKnown(sone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLikesAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..3a75ded
--- /dev/null
@@ -0,0 +1,98 @@
+package net.pterodactylus.sone.web.ajax
+
+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.mock
+import net.pterodactylus.sone.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetLikesAjaxPage].
+ */
+class GetLikesAjaxPageTest : JsonPageTest("getLikes.ajax", needsFormPassword = false, pageSupplier = ::GetLikesAjaxPage) {
+
+       @Test
+       fun `request without parameters results in failing request`() {
+               assertThat(json.isSuccess, equalTo(false))
+       }
+
+       @Test
+       fun `request with invalid post id results in invalid-post-id`() {
+               addRequestParameter("type", "post")
+               addRequestParameter("post", "invalid")
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request with missing post id results in invalid-post-id`() {
+               addRequestParameter("type", "post")
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request with valid post id results in likes for post`() {
+               val post = mock<Post>().apply { whenever(id).thenReturn("post-id") }
+               addPost(post)
+               addLikes(post, createSone(2), createSone(1), createSone(3))
+               addRequestParameter("type", "post")
+               addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["likes"]?.asInt(), equalTo(3))
+               assertThat(json["sones"]!!.toList().map { it["id"].asText() to it["name"].asText() }, contains(
+                               "S1" to "F1 M1 L1",
+                               "S2" to "F2 M2 L2",
+                               "S3" to "F3 M3 L3"
+               ))
+       }
+
+       @Test
+       fun `request with invalid reply id results in invalid-reply-id`() {
+               addRequestParameter("type", "reply")
+               addRequestParameter("reply", "invalid")
+               assertThatJsonFailed("invalid-reply-id")
+       }
+
+       @Test
+       fun `request with missing reply id results in invalid-reply-id`() {
+               addRequestParameter("type", "reply")
+               assertThatJsonFailed("invalid-reply-id")
+       }
+
+       @Test
+       fun `request with valid reply id results in likes for reply`() {
+               val reply = mock<PostReply>().apply { whenever(id).thenReturn("reply-id") }
+               addReply(reply)
+               addLikes(reply, createSone(2), createSone(1), createSone(3))
+               addRequestParameter("type", "reply")
+               addRequestParameter("reply", "reply-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["likes"]?.asInt(), equalTo(3))
+               assertThat(json["sones"]!!.toList().map { it["id"].asText() to it["name"].asText() }, contains(
+                               "S1" to "F1 M1 L1",
+                               "S2" to "F2 M2 L2",
+                               "S3" to "F3 M3 L3"
+               ))
+       }
+
+       @Test
+       fun `request with invalid type results in invalid-type`() {
+               addRequestParameter("type", "invalid")
+               addRequestParameter("invalid", "foo")
+               assertThatJsonFailed("invalid-type")
+       }
+
+}
+
+private fun createSone(index: Int) = mock<Sone>().apply {
+       whenever(id).thenReturn("S$index")
+       whenever(profile).thenReturn(Profile(this).apply {
+               firstName = "F$index"
+               middleName = "M$index"
+               lastName = "L$index"
+       })
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetLinkedElementAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..f49f9f3
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.template.LinkedElementRenderFilter
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.jsonArray
+import net.pterodactylus.util.template.TemplateContext
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+
+/**
+ * Unit test for [GetLinkedElementAjaxPage].
+ */
+class GetLinkedElementAjaxPageTest: JsonPageTest("getLinkedElement.ajax", requiresLogin = false, needsFormPassword = false) {
+
+       private val linkedElementRenderFilter = mock<LinkedElementRenderFilter>()
+       override var page: JsonPage = GetLinkedElementAjaxPage(webInterface, elementLoader, linkedElementRenderFilter)
+
+       @Test
+       fun `only loaded linked elements are returned`() {
+           addRequestParameter("elements", jsonArray("KSK@foo.png", "KSK@foo.jpg", "KSK@foo.html").toString())
+               addLinkedElement("KSK@foo.png", true, false)
+               addLinkedElement("KSK@foo.jpg", false, false)
+               addLinkedElement("KSK@foo.html", false, true)
+               whenever(linkedElementRenderFilter.format(ArgumentMatchers.any<TemplateContext>(), ArgumentMatchers.any(), ArgumentMatchers.any())).thenAnswer { invocation ->
+                       when (invocation.getArgument<LinkedElement>(1).link) {
+                               "KSK@foo.jpg" -> "jpeg-image"
+                               "KSK@foo.html" -> "html-page"
+                               else -> null
+                       }
+               }
+               assertThat(json.get("linkedElements")!!.elements().asSequence().map { it.toMap() }.toList(), Matchers.containsInAnyOrder(
+                               mapOf<String, String?>("link" to "KSK@foo.jpg", "html" to "jpeg-image"),
+                               mapOf("link" to "KSK@foo.html", "html" to "html-page")
+               ))
+       }
+
+       private fun JsonNode.toMap() = fields().asSequence().map { it.key!! to if (it.value.isNull) null else it.value.asText()!! }.toMap()
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..8084b65
--- /dev/null
@@ -0,0 +1,118 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.main.SonePlugin
+import net.pterodactylus.sone.test.argumentCaptor
+import net.pterodactylus.sone.test.get
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.notify.TemplateNotification
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import net.pterodactylus.util.version.Version
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.empty
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.verify
+import java.io.Writer
+
+/**
+ * Unit test for [GetNotificationsAjaxPage].
+ */
+class GetNotificationsAjaxPageTest : JsonPageTest("getNotifications.ajax", requiresLogin = false, needsFormPassword = false, pageSupplier = ::GetNotificationsAjaxPage) {
+
+       private val testNotifications = listOf(
+                       createNotification("n1", 2000, "t1", 5000, true),
+                       createNotification("n2", 1000, "t2", 6000, false),
+                       createNotification("n3", 3000, "t3", 7000, true)
+       )
+
+       private fun createNotification(id: String, createdTime: Long, text: String, lastUpdatedTime: Long, dismissable: Boolean): Notification {
+               return mock<Notification>().apply {
+                       whenever(this.id).thenReturn(id)
+                       whenever(this.createdTime).thenReturn(createdTime)
+                       whenever(this.lastUpdatedTime).thenReturn(lastUpdatedTime)
+                       whenever(this.isDismissable).thenReturn(dismissable)
+                       whenever(render(any())).then { it.get<Writer>(0).write(text) }
+               }
+       }
+
+       @Test
+       fun `notification hash is calculated correctly`() {
+               testNotifications.forEach { addNotification(it) }
+               assertThatJsonIsSuccessful()
+               assertThat(json["notificationHash"]?.asInt(), equalTo(listOf(1, 0, 2).map(testNotifications::get).hashCode()))
+       }
+
+       @Test
+       fun `options are included correctly`() {
+               assertThatJsonIsSuccessful()
+               assertThat(json["options"]!!["ShowNotification/NewSones"].asBoolean(), equalTo(true))
+               assertThat(json["options"]!!["ShowNotification/NewPosts"].asBoolean(), equalTo(true))
+               assertThat(json["options"]!!["ShowNotification/NewReplies"].asBoolean(), equalTo(true))
+       }
+
+       @Test
+       fun `options are included correctly when all false`() {
+               currentSone.options.isShowNewSoneNotifications = false
+               currentSone.options.isShowNewPostNotifications = false
+               currentSone.options.isShowNewReplyNotifications = false
+               assertThatJsonIsSuccessful()
+               assertThat(json["options"]!!["ShowNotification/NewSones"].asBoolean(), equalTo(false))
+               assertThat(json["options"]!!["ShowNotification/NewPosts"].asBoolean(), equalTo(false))
+               assertThat(json["options"]!!["ShowNotification/NewReplies"].asBoolean(), equalTo(false))
+       }
+
+       @Test
+       fun `options are not included if user is not logged in`() {
+               unsetCurrentSone()
+               assertThatJsonIsSuccessful()
+               assertThat(json["options"]?.toList(), empty())
+       }
+
+       @Test
+       fun `notifications are rendered correctly`() {
+               testNotifications.forEach { addNotification(it) }
+               assertThatJsonIsSuccessful()
+               assertThat(json["notifications"]!!.toList().map { node -> listOf("id", "text", "createdTime", "lastUpdatedTime", "dismissable").map { it to node.get(it).asText() }.toMap() }, containsInAnyOrder(
+                               mapOf("id" to "n1", "createdTime" to "2000", "lastUpdatedTime" to "5000", "dismissable" to "true", "text" to "t1"),
+                               mapOf("id" to "n2", "createdTime" to "1000", "lastUpdatedTime" to "6000", "dismissable" to "false", "text" to "t2"),
+                               mapOf("id" to "n3", "createdTime" to "3000", "lastUpdatedTime" to "7000", "dismissable" to "true", "text" to "t3")
+               ))
+       }
+
+       @Test
+       fun `template notifications are rendered correctly`() {
+               whenever(webInterface.templateContextFactory).thenReturn(TemplateContextFactory())
+               whenever(updateChecker.hasLatestVersion()).thenReturn(true)
+               whenever(updateChecker.latestEdition).thenReturn(999)
+               whenever(updateChecker.latestVersion).thenReturn(Version(0, 1, 2))
+               whenever(updateChecker.latestVersionDate).thenReturn(998)
+               val templateNotification = mock<TemplateNotification>().apply {
+                       whenever(id).thenReturn("n4")
+                       whenever(createdTime).thenReturn(4000)
+                       whenever(templateContext).thenReturn(TemplateContext())
+                       whenever(render(any(), any())).then { it.get<Writer>(1).write("t4") }
+               }
+               testNotifications.forEach { addNotification(it) }
+               addNotification(templateNotification)
+               assertThatJsonIsSuccessful()
+               assertThat(json["notifications"]!!.last()["text"].asText(), equalTo("t4"))
+               val templateContext = argumentCaptor<TemplateContext>()
+               verify(templateNotification).render(templateContext.capture(), any())
+               assertThat(templateContext.value["core"], equalTo<Any>(core))
+               assertThat(templateContext.value["currentSone"], equalTo<Any>(currentSone))
+               assertThat(templateContext.value["localSones"], equalTo<Any>(core.localSones))
+               assertThat(templateContext.value["request"], equalTo<Any>(freenetRequest))
+               assertThat(templateContext.value["currentVersion"], equalTo<Any>(SonePlugin.getPluginVersion()))
+               assertThat(templateContext.value["hasLatestVersion"], equalTo<Any>(true))
+               assertThat(templateContext.value["latestEdition"], equalTo<Any>(999L))
+               assertThat(templateContext.value["latestVersion"], equalTo<Any>(Version(0, 1, 2)))
+               assertThat(templateContext.value["latestVersionTime"], equalTo<Any>(998L))
+               assertThat(templateContext.value["notification"], equalTo<Any>(templateNotification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetPostAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..8d33542
--- /dev/null
@@ -0,0 +1,54 @@
+package net.pterodactylus.sone.web.ajax
+
+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.utils.asOptional
+import net.pterodactylus.sone.utils.asTemplate
+import net.pterodactylus.util.template.ReflectionAccessor
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetPostAjaxPage].
+ */
+class GetPostAjaxPageTest : JsonPageTest("getPost.ajax", needsFormPassword = false,
+               pageSupplier = { webInterface ->
+                       GetPostAjaxPage(webInterface, "<%core>\n<%request>\n<%post.text>\n<%currentSone>\n<%localSones>".asTemplate())
+               }) {
+
+       @Test
+       fun `request with missing post results in invalid-post-id`() {
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request with valid post results in post json`() {
+               val sone = mock<Sone>().apply { whenever(id).thenReturn("sone-id") }
+               val post = mock<Post>().apply {
+                       whenever(id).thenReturn("post-id")
+                       whenever(time).thenReturn(1000)
+                       whenever(this.sone).thenReturn(sone)
+                       whenever(recipientId).thenReturn("recipient-id".asOptional())
+                       whenever(text).thenReturn("post text")
+               }
+               webInterface.templateContextFactory.addAccessor(Any::class.java, ReflectionAccessor())
+               addPost(post)
+               addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["post"]!!["id"].asText(), equalTo("post-id"))
+               assertThat(json["post"]!!["time"].asLong(), equalTo(1000L))
+               assertThat(json["post"]!!["sone"].asText(), equalTo("sone-id"))
+               assertThat(json["post"]!!["recipient"].asText(), equalTo("recipient-id"))
+               assertThat(json["post"]!!["html"].asText(), equalTo(listOf(
+                               core.toString(),
+                               freenetRequest.toString(),
+                               "post text",
+                               currentSone.toString(),
+                               core.localSones.toString()
+               ).joinToString("\n")))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetReplyAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..6af8776
--- /dev/null
@@ -0,0 +1,52 @@
+package net.pterodactylus.sone.web.ajax
+
+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.utils.asTemplate
+import net.pterodactylus.util.template.ReflectionAccessor
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetReplyAjaxPage].
+ */
+class GetReplyAjaxPageTest : JsonPageTest("getReply.ajax", needsFormPassword = false,
+               pageSupplier = { webInterface ->
+                       GetReplyAjaxPage(webInterface, "<%core>\n<%request>\n<%reply.text>\n<%currentSone>".asTemplate())
+               }) {
+
+       @Test
+       fun `request without reply id results in invalid-reply-id`() {
+               assertThatJsonFailed("invalid-reply-id")
+       }
+
+       @Test
+       fun `request with valid reply id results in reply json`() {
+               val sone = mock<Sone>().apply { whenever(id).thenReturn("sone-id") }
+               val reply = mock<PostReply>().apply {
+                       whenever(id).thenReturn("reply-id")
+                       whenever(this.sone).thenReturn(sone)
+                       whenever(postId).thenReturn("post-id")
+                       whenever(time).thenReturn(1000)
+                       whenever(text).thenReturn("reply text")
+               }
+               webInterface.templateContextFactory.addAccessor(Any::class.java, ReflectionAccessor())
+               addReply(reply)
+               addRequestParameter("reply", "reply-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["reply"]!!["id"].asText(), equalTo("reply-id"))
+               assertThat(json["reply"]!!["soneId"].asText(), equalTo("sone-id"))
+               assertThat(json["reply"]!!["postId"].asText(), equalTo("post-id"))
+               assertThat(json["reply"]!!["time"].asLong(), equalTo(1000L))
+               assertThat(json["reply"]!!["html"].asText(), equalTo(listOf(
+                               core.toString(),
+                               freenetRequest.toString(),
+                               "reply text",
+                               currentSone.toString()
+               ).joinToString("\n")))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetStatusAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..9576c7c
--- /dev/null
@@ -0,0 +1,139 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.Sone.SoneStatus.downloading
+import net.pterodactylus.sone.data.Sone.SoneStatus.inserting
+import net.pterodactylus.sone.freenet.L10nFilter
+import net.pterodactylus.sone.freenet.L10nText
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.text.TimeText
+import net.pterodactylus.sone.text.TimeTextConverter
+import net.pterodactylus.sone.utils.jsonArray
+import net.pterodactylus.util.notify.Notification
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.emptyIterable
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasEntry
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import java.util.TimeZone
+
+/**
+ * Unit test for [GetStatusAjaxPage].
+ */
+class GetStatusAjaxPageTest: JsonPageTest("getStatus.ajax", requiresLogin = false, needsFormPassword = false) {
+
+       private val timeTextConverter = mock<TimeTextConverter>()
+       private val l10nFilter = mock<L10nFilter>()
+       override var page: JsonPage = GetStatusAjaxPage(webInterface, elementLoader, timeTextConverter, l10nFilter, TimeZone.getTimeZone("UTC"))
+
+       @Before
+       fun setupTimeTextConverter() {
+               whenever(timeTextConverter.getTimeText(anyLong())).thenAnswer { TimeText(L10nText(it.getArgument<Long>(0).toString()), it.getArgument(0)) }
+               whenever(l10nFilter.format(any(), any(), any())).thenAnswer { it.getArgument<L10nText>(1).text }
+       }
+
+       @Test
+       fun `page returns correct attribute “loggedIn” if sone is logged in`() {
+               assertThat(json.get("loggedIn")?.asText(), equalTo("true"))
+       }
+
+       @Test
+       fun `page returns correct attribute “loggedIn” if sone is not logged in`() {
+               unsetCurrentSone()
+               assertThat(json.get("loggedIn")?.asText(), equalTo("false"))
+       }
+
+       @Test
+       fun `page returns options for sone if sone is logged in`() {
+               assertThat(json.get("options")?.toMap(), allOf(
+                               hasEntry("ShowNotification/NewSones", "true"),
+                               hasEntry("ShowNotification/NewPosts", "true"),
+                               hasEntry("ShowNotification/NewReplies", "true")
+               ))
+       }
+
+       @Test
+       fun `page returns empty options if sone is not logged in`() {
+               unsetCurrentSone()
+               assertThat(json.get("options"), emptyIterable())
+       }
+
+       @Test
+       fun `page returns empty sones object if no sone is logged in and no sones parameter is given`() {
+               unsetCurrentSone()
+               assertThat(json.get("sones"), emptyIterable())
+       }
+
+       @Test
+       fun `page returns a sones object with the current sone if not other sones parameter is given`() {
+               assertThat(json.get("sones")!!.elements().asSequence().map { it.toMap() }.toList(), containsInAnyOrder(
+                               mapOf<String, String?>("id" to "soneId", "name" to "Sone_Id", "local" to "true", "status" to "idle", "modified" to "false", "locked" to "false", "lastUpdatedUnknown" to "false", "lastUpdated" to "Jan 1, 1970, 00:00:01", "lastUpdatedText" to "1000")
+               ))
+       }
+
+       @Test
+       fun `page returns some sones objects with the current sone and some sones given as sones parameter`() {
+               addSone(deepMock<Sone>().mock("sone1", "Sone 1", false, 2000, downloading))
+               addSone(deepMock<Sone>().mock("sone3", "Sone 3", true, 3000, inserting))
+               addRequestParameter("soneIds", "sone1,sone2,sone3")
+               assertThat(json.get("sones")!!.elements().asSequence().map { it.toMap() }.toList(), containsInAnyOrder(
+                               mapOf<String, String?>("id" to "soneId", "name" to "Sone_Id", "local" to "true", "status" to "idle", "modified" to "false", "locked" to "false", "lastUpdatedUnknown" to "false", "lastUpdated" to "Jan 1, 1970, 00:00:01", "lastUpdatedText" to "1000"),
+                               mapOf("id" to "sone1", "name" to "Sone 1", "local" to "false", "status" to "downloading", "modified" to "false", "locked" to "false", "lastUpdatedUnknown" to "false", "lastUpdated" to "Jan 1, 1970, 00:00:02", "lastUpdatedText" to "2000"),
+                               mapOf("id" to "sone3", "name" to "Sone 3", "local" to "true", "status" to "inserting", "modified" to "false", "locked" to "false", "lastUpdatedUnknown" to "false", "lastUpdated" to "Jan 1, 1970, 00:00:03", "lastUpdatedText" to "3000")
+               ))
+       }
+
+       @Test
+       fun `page returns correct notifications hash`() {
+               val notifications = listOf(
+                               mock<Notification>().apply { whenever(this.createdTime).thenReturn(2000) },
+                               mock<Notification>().apply { whenever(this.createdTime).thenReturn(1000) }
+               )
+               notifications.forEachIndexed { index, notification -> addNotification(notification, "notification$index")}
+               assertThat(json.get("notificationHash")?.asInt(), equalTo(notifications.sortedBy { it.createdTime }.hashCode()))
+       }
+
+       @Test
+       fun `page returns new posts`() {
+               addNewPost("post1", "sone1", 1000)
+               addNewPost("post2", "sone2", 2000, "sone1")
+               assertThat(json.get("newPosts")!!.elements().asSequence().map { it.toMap() }.toList(), containsInAnyOrder(
+                               mapOf("id" to "post1", "sone" to "sone1", "time" to "1000", "recipient" to null),
+                               mapOf("id" to "post2", "sone" to "sone2", "time" to "2000", "recipient" to "sone1")
+               ))
+       }
+
+       @Test
+       fun `page returns new replies`() {
+               addNewReply("reply1", "sone1", "post1", "sone11")
+               addNewReply("reply2", "sone2", "post2", "sone22")
+               assertThat(json.get("newReplies")!!.elements().asSequence().map { it.toMap() }.toList(), containsInAnyOrder(
+                               mapOf<String, String?>("id" to "reply1", "sone" to "sone1", "post" to "post1", "postSone" to "sone11"),
+                               mapOf("id" to "reply2", "sone" to "sone2", "post" to "post2", "postSone" to "sone22")
+               ))
+       }
+
+       @Test
+       fun `page returns information about loaded elements`() {
+               addLinkedElement("KSK@test.png", loading = false, failed = false)
+               addLinkedElement("KSK@test.html", loading = true, failed = false)
+               addLinkedElement("KSK@test.jpeg", loading = false, failed = true)
+               addRequestParameter("elements", jsonArray("KSK@test.png", "KSK@test.html", "KSK@test.jpeg").toString())
+               assertThat(json.get("linkedElements")!!.elements().asSequence().map { it.toMap() }.toList(), containsInAnyOrder(
+                               mapOf<String, String?>("link" to "KSK@test.png", "loading" to "false", "failed" to "false"),
+                               mapOf("link" to "KSK@test.html", "loading" to "true", "failed" to "false"),
+                               mapOf("link" to "KSK@test.jpeg", "loading" to "false", "failed" to "true")
+               ))
+       }
+
+       private fun JsonNode.toMap() = fields().asSequence().map { it.key!! to if (it.value.isNull) null else it.value.asText()!! }.toMap()
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTimesAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..fff438b
--- /dev/null
@@ -0,0 +1,110 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.freenet.L10nFilter
+import net.pterodactylus.sone.freenet.L10nText
+import net.pterodactylus.sone.test.get
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.text.TimeText
+import net.pterodactylus.sone.text.TimeTextConverter
+import net.pterodactylus.sone.utils.jsonObject
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.emptyIterable
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import java.util.TimeZone.getTimeZone
+
+/**
+ * Unit test for [GetTimesAjaxPage].
+ */
+class GetTimesAjaxPageTest : JsonPageTest("getTimes.ajax", needsFormPassword = false, requiresLogin = false) {
+
+       private val timeTextConverter = mock<TimeTextConverter>()
+       private val l10nFilter = mock<L10nFilter>()
+       override val page: JsonPage by lazy { GetTimesAjaxPage(webInterface, timeTextConverter, l10nFilter, getTimeZone("UTC")) }
+       private val testPosts = listOf(createPost(1), createPost(2))
+       private val testReplies = listOf(createReply(1), createReply(2))
+
+       private fun createPost(index: Int): Post {
+               return mock<Post>().apply {
+                       whenever(id).thenReturn("post$index")
+                       whenever(time).thenReturn(index.toLong() * 1000)
+               }
+       }
+
+       private fun createReply(index: Int): PostReply {
+               return mock<PostReply>().apply {
+                       whenever(id).thenReturn("reply$index")
+                       whenever(time).thenReturn(index.toLong() * 1000)
+               }
+       }
+
+       @Before
+       fun setupMocks() {
+               whenever(timeTextConverter.getTimeText(anyLong())).then { TimeText(L10nText(it.get<Long>(0).toString()), it.get<Long>(0) * 2) }
+               whenever(l10nFilter.format(any(), any(), any())).then { it.get<L10nText>(1).text }
+       }
+
+       @Test
+       fun `request without any parameters responds with empty post and reply times`() {
+               assertThatJsonIsSuccessful()
+               assertThat(json["postTimes"]?.toList(), emptyIterable())
+               assertThat(json["replyTimes"]?.toList(), emptyIterable())
+       }
+
+       @Test
+       fun `request with single post parameter responds with post times and empty reply times`() {
+               addPost(testPosts[0])
+               addRequestParameter("posts", "post1")
+               assertThatJsonIsSuccessful()
+               assertThat(json["postTimes"]!!.fields().asSequence().map { it.key to it.value }.toList(), containsInAnyOrder<Pair<String, JsonNode>>(
+                               "post1" to jsonObject("timeText" to "1000", "refreshTime" to 2L, "tooltip" to "Jan 1, 1970, 00:00:01")
+               ))
+               assertThat(json["replyTimes"]?.toList(), emptyIterable())
+       }
+
+       @Test
+       fun `request with single reply parameter responds with reply times and empty post times`() {
+               addReply(testReplies[0])
+               addRequestParameter("replies", "reply1")
+               assertThatJsonIsSuccessful()
+               assertThat(json["postTimes"]?.toList(), emptyIterable())
+               assertThat(json["replyTimes"]!!.fields().asSequence().map { it.key to it.value }.toList(), containsInAnyOrder<Pair<String, JsonNode>>(
+                               "reply1" to jsonObject("timeText" to "1000", "refreshTime" to 2L, "tooltip" to "Jan 1, 1970, 00:00:01")
+               ))
+       }
+
+       @Test
+       fun `request with multiple post parameter responds with post times and empty reply times`() {
+               addPost(testPosts[0])
+               addPost(testPosts[1])
+               addRequestParameter("posts", "post1,post2,post3")
+               assertThatJsonIsSuccessful()
+               assertThat(json["postTimes"]!!.fields().asSequence().map { it.key to it.value }.toList(), containsInAnyOrder<Pair<String, JsonNode>>(
+                               "post1" to jsonObject("timeText" to "1000", "refreshTime" to 2L, "tooltip" to "Jan 1, 1970, 00:00:01"),
+                               "post2" to jsonObject("timeText" to "2000", "refreshTime" to 4L, "tooltip" to "Jan 1, 1970, 00:00:02")
+               ))
+               assertThat(json["replyTimes"]?.toList(), emptyIterable())
+       }
+
+       @Test
+       fun `request with multiple reply parameters responds with reply times and empty post times`() {
+               addReply(testReplies[0])
+               addReply(testReplies[1])
+               addRequestParameter("replies", "reply1,reply2,reply3")
+               assertThatJsonIsSuccessful()
+               assertThat(json["postTimes"]?.toList(), emptyIterable())
+               assertThat(json["replyTimes"]!!.fields().asSequence().map { it.key to it.value }.toList(), containsInAnyOrder<Pair<String, JsonNode>>(
+                               "reply1" to jsonObject("timeText" to "1000", "refreshTime" to 2L, "tooltip" to "Jan 1, 1970, 00:00:01"),
+                               "reply2" to jsonObject("timeText" to "2000", "refreshTime" to 4L, "tooltip" to "Jan 1, 1970, 00:00:02")
+               ))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/GetTranslationAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..0d58ca2
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetTranslationAjaxPage].
+ */
+class GetTranslationAjaxPageTest : JsonPageTest("getTranslation.ajax", requiresLogin = false, needsFormPassword = false, pageSupplier = ::GetTranslationAjaxPage) {
+
+       @Test
+       fun `translation is returned correctly`() {
+               addTranslation("foo", "bar")
+               addRequestParameter("key", "foo")
+               assertThatJsonIsSuccessful()
+               assertThat(json["value"]?.asText(), equalTo("bar"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObjectTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonErrorReturnObjectTest.kt
new file mode 100644 (file)
index 0000000..bf61e2e
--- /dev/null
@@ -0,0 +1,24 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [JsonErrorReturnObject].
+ */
+class JsonErrorReturnObjectTest {
+
+       private val returnObject = JsonErrorReturnObject("nope")
+
+       @Test
+       fun `error return object is not successful`() {
+               assertThat(returnObject.isSuccess, equalTo(false))
+       }
+
+       @Test
+       fun `error return object exposes error`() {
+               assertThat(returnObject.error, equalTo("nope"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageBaseTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageBaseTest.kt
new file mode 100644 (file)
index 0000000..75a0cd4
--- /dev/null
@@ -0,0 +1,135 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.core.Preferences
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.web.Response
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Unit test for [JsonPage].
+ */
+class JsonPageBaseTest : TestObjects() {
+
+       private var needsFormPassword = false
+       private val pageCallCounter = AtomicInteger()
+       private var pageResponse = { JsonReturnObject(true).put("foo", "bar") }
+       private val outputStream = ByteArrayOutputStream()
+       private val response = Response(outputStream)
+
+       private val page = object : JsonPage("path.html", webInterface) {
+
+               override val needsFormPassword get() = this@JsonPageBaseTest.needsFormPassword
+
+               override fun createJsonObject(request: FreenetRequest) =
+                               pageResponse().also { pageCallCounter.incrementAndGet() }
+
+       }
+
+       @Before
+       fun setupWebInterface() {
+               whenever(webInterface.core).thenReturn(core)
+       }
+
+       @Before
+       fun setupCore() {
+               whenever(core.preferences).thenReturn(Preferences(eventBus))
+       }
+
+       @Before
+       fun setupFreenetRequest() {
+               whenever(freenetRequest.toadletContext).thenReturn(toadletContext)
+       }
+
+       @Test
+       fun `page returns 403 is full access is required but request is not full access`() {
+               core.preferences.isRequireFullAccess = true
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(403))
+               assertThat(response.statusText, equalTo("Forbidden"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to false, "error" to "auth-required")))
+       }
+
+       @Test
+       fun `page returns 403 if form password is needed but not supplied`() {
+               needsFormPassword = true
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(403))
+               assertThat(response.statusText, equalTo("Forbidden"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to false, "error" to "auth-required")))
+       }
+
+       @Test
+       fun `page returns 403 is form password is supplied but incorrect`() {
+               needsFormPassword = true
+               addRequestParameter("formPassword", formPassword + "_false")
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(403))
+               assertThat(response.statusText, equalTo("Forbidden"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to false, "error" to "auth-required")))
+       }
+
+       @Test
+       fun `page returns 200 if form password is required and correct`() {
+               needsFormPassword = true
+               addRequestParameter("formPassword", formPassword)
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.statusText, equalTo("OK"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to true, "foo" to "bar")))
+       }
+
+       @Test
+       fun `page returns 403 is login is required but current Sone is null`() {
+               unsetCurrentSone()
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(403))
+               assertThat(response.statusText, equalTo("Forbidden"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to false, "error" to "auth-required")))
+       }
+
+       @Test
+       fun `page returns content if login is required and current Sone is set`() {
+               page.handleRequest(freenetRequest, response)
+               assertThat(pageCallCounter.get(), equalTo(1))
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.statusText, equalTo("OK"))
+               assertThat(response.contentType, equalTo("application/json"))
+               assertThat(outputStream.toString("UTF-8").asJson(), equalTo(mapOf("success" to true, "foo" to "bar")))
+       }
+
+       @Test
+       fun `page returns 500 if execution throws exception`() {
+               pageResponse = { throw IllegalStateException("some error occured") }
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(500))
+               assertThat(response.statusText, equalTo("some error occured"))
+               assertThat(response.contentType, equalTo("text/plain"))
+       }
+
+       @Test
+       fun `page returns stack trace if execution throws exception`() {
+               pageResponse = { throw IllegalStateException() }
+               page.handleRequest(freenetRequest, response)
+               assertThat(outputStream.toString(), containsString("IllegalStateException"))
+       }
+
+       @Test
+       fun `json page is not a prefix page`() {
+           assertThat(page.isPrefixPage, equalTo(false))
+       }
+
+       private fun String.asJson() = objectMapper.readValue(this, Map::class.java) as Map<String, Any>
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonPageTest.kt
new file mode 100644 (file)
index 0000000..ea882d3
--- /dev/null
@@ -0,0 +1,49 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.web.WebInterface
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Base class for tests for any [JsonPage] implementations.
+ */
+abstract class JsonPageTest(
+               private val expectedPath: String,
+               private val requiresLogin: Boolean = true,
+               private val needsFormPassword: Boolean = true,
+               pageSupplier: (WebInterface) -> JsonPage = { mock() }) : TestObjects() {
+
+       protected open val page: JsonPage by lazy { pageSupplier(webInterface) }
+       protected val json by lazy {
+               page.createJsonObject(freenetRequest)
+       }
+
+       private val JsonReturnObject.error get() = (this as? JsonErrorReturnObject)?.error
+
+       protected fun assertThatJsonIsSuccessful() {
+               assertThat(json.isSuccess, equalTo(true))
+       }
+
+       protected fun assertThatJsonFailed(error: String? = null) {
+               assertThat(json.isSuccess, equalTo(false))
+               error?.run { assertThat(json.error, equalTo(this)) }
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo(expectedPath))
+       }
+
+       @Test
+       fun `page needs form password`() {
+               assertThat(page.needsFormPassword, equalTo(needsFormPassword))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin, equalTo(requiresLogin))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObjectTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/JsonReturnObjectTest.kt
new file mode 100644 (file)
index 0000000..7849139
--- /dev/null
@@ -0,0 +1,151 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.BooleanNode
+import com.fasterxml.jackson.databind.node.IntNode
+import com.fasterxml.jackson.databind.node.JsonNodeFactory
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.databind.node.TextNode
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.not
+import org.junit.Test
+
+/**
+ * Unit test for [JsonReturnObject].
+ */
+class JsonReturnObjectTest {
+
+       private val jsonReturnObject = JsonReturnObject(true)
+       private val objectMapper = ObjectMapper()
+
+       @Test
+       fun `json object retains success status if true`() {
+               assertThat(JsonReturnObject(true).isSuccess, equalTo(true))
+       }
+
+       @Test
+       fun `json object retains success status if false`() {
+               assertThat(JsonReturnObject(false).isSuccess, equalTo(false))
+       }
+
+       @Test
+       fun `json object returns text nodes for string properties`() {
+               jsonReturnObject.put("foo", "bar")
+               assertThat(jsonReturnObject["foo"], equalTo<Any>(TextNode("bar")))
+       }
+
+       @Test
+       fun `json object returns int nodes for int properties`() {
+               jsonReturnObject.put("foo", 123)
+               assertThat(jsonReturnObject["foo"], equalTo<Any>(IntNode(123)))
+       }
+
+       @Test
+       fun `json object returns boolean nodes for boolean properties`() {
+               jsonReturnObject.put("foo", true)
+               assertThat(jsonReturnObject["foo"], equalTo<Any>(BooleanNode.TRUE))
+       }
+
+       @Test
+       fun `json object returns json node for json properties`() {
+               val objectNode = ObjectNode(JsonNodeFactory.instance)
+               jsonReturnObject.put("foo", objectNode)
+               assertThat(jsonReturnObject["foo"], equalTo<Any>(objectNode))
+       }
+
+       @Test
+       fun `json object returns all properties`() {
+               val objectNode = ObjectNode(JsonNodeFactory.instance)
+               jsonReturnObject.put("text", "text")
+               jsonReturnObject.put("int", 123)
+               jsonReturnObject.put("boolean", true)
+               jsonReturnObject.put("object", objectNode)
+               assertThat(jsonReturnObject.content, equalTo<Any>(mapOf(
+                               "text" to TextNode("text"),
+                               "int" to IntNode(123),
+                               "boolean" to BooleanNode.TRUE,
+                               "object" to objectNode
+               )))
+       }
+
+       @Test
+       fun `json object is serialized correctly`() {
+               val objectNode = ObjectNode(JsonNodeFactory.instance)
+               jsonReturnObject.put("text", "text")
+               jsonReturnObject.put("int", 123)
+               jsonReturnObject.put("boolean", true)
+               jsonReturnObject.put("object", objectNode)
+               val json = objectMapper.writeValueAsString(jsonReturnObject)
+               val parsedJson = objectMapper.readTree(json)
+               assertThat(parsedJson, equalTo<JsonNode>(ObjectNode(JsonNodeFactory.instance).apply {
+                       put("success", true)
+                       put("text", "text")
+                       put("int", 123)
+                       put("boolean", true)
+                       set("object", objectNode)
+               }))
+       }
+
+       @Test
+       fun `successful object is not equal to unsuccessful object`() {
+               assertThat(JsonReturnObject(true), not(equalTo(JsonReturnObject(false))))
+       }
+
+       @Test
+       fun `objects with different content are not equal`() {
+               val firstObject = JsonReturnObject(true).apply {
+                       put("text", "text")
+               }
+               val secondObject = JsonReturnObject(true).apply {
+                       put("number", 123)
+               }
+               assertThat(firstObject, not(equalTo(secondObject)))
+       }
+
+       @Test
+       fun `object is not equal to null`() {
+           assertThat(JsonReturnObject(true), not(equalTo<Any?>(null)))
+       }
+
+       @Test
+       fun `object is not equal to object of different class`() {
+           assertThat(JsonReturnObject(true), not(equalTo<Any>("string")))
+       }
+
+       @Test
+       fun `equals is correctly implemented`() {
+               val firstObject = JsonReturnObject(true).apply {
+                       put("text", "text")
+                       put("int", 123)
+                       put("boolean", true)
+                       put("object", ObjectNode(JsonNodeFactory.instance))
+               }
+               val secondObject = JsonReturnObject(true).apply {
+                       put("text", "text")
+                       put("int", 123)
+                       put("boolean", true)
+                       put("object", ObjectNode(JsonNodeFactory.instance))
+               }
+               assertThat(firstObject, equalTo(secondObject))
+       }
+
+       @Test
+       fun `hash code of equal objects is equal`() {
+               val firstObject = JsonReturnObject(true).apply {
+                       put("text", "text")
+                       put("int", 123)
+                       put("boolean", true)
+                       put("object", ObjectNode(JsonNodeFactory.instance))
+               }
+               val secondObject = JsonReturnObject(true).apply {
+                       put("text", "text")
+                       put("int", 123)
+                       put("boolean", true)
+                       put("object", ObjectNode(JsonNodeFactory.instance))
+               }
+               assertThat(firstObject.hashCode(), equalTo(secondObject.hashCode()))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/LikeAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..457aac4
--- /dev/null
@@ -0,0 +1,63 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+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 [LikeAjaxPage].
+ */
+class LikeAjaxPageTest : JsonPageTest("like.ajax", pageSupplier = ::LikeAjaxPage) {
+
+       @Test
+       fun `request with invalid type results in invalid-type error`() {
+               addRequestParameter("type", "invalid")
+               addRequestParameter("invalid", "invalid-id")
+               assertThatJsonFailed("invalid-type")
+       }
+
+       @Test
+       fun `request with valid post id results in post being liked by current sone`() {
+               addRequestParameter("type", "post")
+               addRequestParameter("post", "post-id")
+               addPost(mock<Post>().apply { whenever(id).thenReturn("post-id") })
+               assertThatJsonIsSuccessful()
+               verify(currentSone).addLikedPostId("post-id")
+               verify(core).touchConfiguration()
+       }
+
+       @Test
+       fun `request with valid reply id results in reply being liked by current sone`() {
+               addRequestParameter("type", "reply")
+               addRequestParameter("reply", "reply-id")
+               addReply(mock<PostReply>().apply { whenever(id).thenReturn("reply-id") })
+               assertThatJsonIsSuccessful()
+               verify(currentSone).addLikedReplyId("reply-id")
+               verify(core).touchConfiguration()
+       }
+
+       @Test
+       fun `request with invalid post id results in post being liked by current sone`() {
+               addRequestParameter("type", "post")
+               addRequestParameter("post", "post-id")
+               assertThat(json.isSuccess, equalTo(false))
+               verify(currentSone, never()).addLikedPostId("post-id")
+               verify(core, never()).touchConfiguration()
+       }
+
+       @Test
+       fun `request with invalid reply id results in reply being liked by current sone`() {
+               addRequestParameter("type", "reply")
+               addRequestParameter("reply", "reply-id")
+               assertThat(json.isSuccess, equalTo(false))
+               verify(currentSone, never()).addLikedReplyId("reply-id")
+               verify(core, never()).touchConfiguration()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/LockSoneAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..3a2a524
--- /dev/null
@@ -0,0 +1,29 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [LockSoneAjaxPage].
+ */
+class LockSoneAjaxPageTest : JsonPageTest("lockSone.ajax", requiresLogin = false, pageSupplier = ::LockSoneAjaxPage) {
+
+       @Test
+       fun `request without valid sone results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone id results in locked sone`() {
+               val sone = mock<Sone>()
+               addLocalSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).lockSone(sone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/LoggedInJsonPageTest.kt
new file mode 100644 (file)
index 0000000..9578d45
--- /dev/null
@@ -0,0 +1,6 @@
+package net.pterodactylus.sone.web.ajax
+
+/**
+ * Unit test for [LoggedInJsonPageTest].
+ */
+class LoggedInJsonPageTest : JsonPageTest("path", requiresLogin = true, pageSupplier = { webInterface -> LoggedInJsonPage("path", webInterface) })
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..a3ab0ff
--- /dev/null
@@ -0,0 +1,72 @@
+package net.pterodactylus.sone.web.ajax
+
+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.test.whenever
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [MarkAsKnownAjaxPage].
+ */
+class MarkAsKnownAjaxPageTest : JsonPageTest("markAsKnown.ajax", requiresLogin = false, pageSupplier = ::MarkAsKnownAjaxPage) {
+
+       @Test
+       fun `request without type results in invalid-type`() {
+               assertThatJsonFailed("invalid-type")
+       }
+
+       @Test
+       fun `request with unknown sone returns successfully`() {
+               addRequestParameter("type", "sone")
+               addRequestParameter("id", "invalid")
+               assertThatJsonIsSuccessful()
+               verify(core, never()).markSoneKnown(any())
+       }
+
+       @Test
+       fun `request with multiple valid sones marks sones as known and returns successfully`() {
+               addRequestParameter("type", "sone")
+               addRequestParameter("id", "sone-id1 sone-id2")
+               val sone1 = mock<Sone>().apply { whenever(id).thenReturn("sone-id1") }
+               val sone2 = mock<Sone>().apply { whenever(id).thenReturn("sone-id2") }
+               addSone(sone1)
+               addSone(sone2)
+               assertThatJsonIsSuccessful()
+               verify(core).markSoneKnown(sone1)
+               verify(core).markSoneKnown(sone2)
+       }
+
+       @Test
+       fun `request with multiple valid posts marks posts as known and returns successfully`() {
+               addRequestParameter("type", "post")
+               addRequestParameter("id", "post1 post2 post 3")
+               val post1 = mock<Post>()
+               val post2 = mock<Post>()
+               addPost(post1, "post1")
+               addPost(post2, "post2")
+               assertThatJsonIsSuccessful()
+               verify(core).markPostKnown(post1)
+               verify(core).markPostKnown(post2)
+       }
+
+       @Test
+       fun `request with multiple valid replies marks replies as known and returns successfully`() {
+               addRequestParameter("type", "reply")
+               addRequestParameter("id", "reply1 reply2 reply3")
+               val reply1 = mock<PostReply>()
+               val reply2 = mock<PostReply>()
+               addReply(reply1, "reply1")
+               addReply(reply2, "reply2")
+               assertThatJsonIsSuccessful()
+               verify(core).markReplyKnown(reply1)
+               verify(core).markReplyKnown(reply2)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..aab1bbe
--- /dev/null
@@ -0,0 +1,65 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [MoveProfileFieldAjaxPage].
+ */
+class MoveProfileFieldAjaxPageTest : JsonPageTest("moveProfileField.ajax", true, true, ::MoveProfileFieldAjaxPage) {
+
+       @Test
+       fun `request without field id results in invalid-field-id`() {
+               assertThatJsonFailed("invalid-field-id")
+       }
+
+       @Test
+       fun `request with invalid direction results in invalid-direction`() {
+               val fieldId = profile.addField("someField").id
+               addRequestParameter("field", fieldId)
+               assertThatJsonFailed("invalid-direction")
+       }
+
+       @Test
+       fun `moving first field up results in not-possible`() {
+               val fieldId = profile.addField("someField").id
+               addRequestParameter("field", fieldId)
+               addRequestParameter("direction", "up")
+               assertThatJsonFailed("not-possible")
+       }
+
+       @Test
+       fun `moving only field down results in not-possible`() {
+               val fieldId = profile.addField("someField").id
+               addRequestParameter("field", fieldId)
+               addRequestParameter("direction", "down")
+               assertThatJsonFailed("not-possible")
+       }
+
+       @Test
+       fun `moving second field up results in field being moved up`() {
+               profile.addField("firstField")
+               val fieldId = profile.addField("someField").id
+               addRequestParameter("field", fieldId)
+               addRequestParameter("direction", "up")
+               assertThatJsonIsSuccessful()
+               assertThat(profile.fields[0].id, equalTo(fieldId))
+               verify(core).touchConfiguration()
+               verify(currentSone).profile = profile
+       }
+
+       @Test
+       fun `moving first field down results in field being moved down`() {
+               val fieldId = profile.addField("someField").id
+               profile.addField("firstField")
+               addRequestParameter("field", fieldId)
+               addRequestParameter("direction", "down")
+               assertThatJsonIsSuccessful()
+               assertThat(profile.fields.last().id, equalTo(fieldId))
+               verify(core).touchConfiguration()
+               verify(currentSone).profile = profile
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TestObjects.kt
new file mode 100644 (file)
index 0000000..1cbba3d
--- /dev/null
@@ -0,0 +1,229 @@
+package net.pterodactylus.sone.web.ajax
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.google.common.eventbus.EventBus
+import freenet.clients.http.ToadletContext
+import freenet.l10n.BaseL10n
+import freenet.support.SimpleReadOnlyArrayBucket
+import freenet.support.api.HTTPRequest
+import net.pterodactylus.sone.core.Core
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+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.Profile
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.Sone.SoneStatus
+import net.pterodactylus.sone.data.Sone.SoneStatus.idle
+import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.get
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import net.pterodactylus.sone.web.WebInterface
+import net.pterodactylus.sone.web.page.FreenetRequest
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.template.TemplateContextFactory
+import net.pterodactylus.util.web.Method.GET
+import net.pterodactylus.util.web.Method.POST
+import org.mockito.ArgumentMatchers
+import java.util.NoSuchElementException
+import javax.naming.SizeLimitExceededException
+
+/**
+ * Base class for tests that supplies commonly used objects.
+ */
+open class TestObjects {
+
+       val objectMapper = ObjectMapper()
+
+       val webInterface = mock<WebInterface>()
+       var formPassword = "form-password"
+       val l10n = mock<BaseL10n>()
+       val core = mock<Core>()
+       val eventBus = mock<EventBus>()
+       val preferences = Preferences(eventBus)
+       val updateChecker = mock<UpdateChecker>()
+       val elementLoader = mock<ElementLoader>()
+
+       val toadletContext = mock<ToadletContext>()
+       val freenetRequest = mock<FreenetRequest>()
+       val httpRequest = mock<HTTPRequest>()
+       val currentSone = deepMock<Sone>()
+       val profile = Profile(currentSone)
+
+       val requestHeaders = mutableMapOf<String, String>()
+       val requestParameters = mutableMapOf<String, String>()
+       val requestParts = mutableMapOf<String, String>()
+       val localSones = mutableMapOf<String, Sone>()
+       val remoteSones = mutableMapOf<String, Sone>()
+       val posts = mutableMapOf<String, Post>()
+       val postLikes = mutableMapOf<Post, Set<Sone>>()
+       val newPosts = mutableMapOf<String, Post>()
+       val replies = mutableMapOf<String, PostReply>()
+       val replyLikes = mutableMapOf<PostReply, Set<Sone>>()
+       val newReplies = mutableMapOf<String, PostReply>()
+       val linkedElements = mutableMapOf<String, LinkedElement>()
+       val notifications = mutableMapOf<String, Notification>()
+       val albums = mutableMapOf<String, Album>()
+       val images = mutableMapOf<String, Image>()
+       val translations = mutableMapOf<String, String>()
+
+       init {
+               whenever(webInterface.templateContextFactory).thenReturn(TemplateContextFactory())
+               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(currentSone)
+               whenever(webInterface.core).thenReturn(core)
+               whenever(webInterface.formPassword).then { formPassword }
+               whenever(webInterface.getNotifications(currentSone)).thenAnswer { notifications.values }
+               whenever(webInterface.getNotification(ArgumentMatchers.anyString())).then { notifications[it[0]].asOptional() }
+               whenever(webInterface.getNewPosts(currentSone)).thenAnswer { newPosts.values }
+               whenever(webInterface.getNewReplies(currentSone)).thenAnswer { newReplies.values }
+               whenever(webInterface.l10n).thenReturn(l10n)
+
+               whenever(l10n.getString(ArgumentMatchers.anyString())).then { translations[it[0]] }
+
+               whenever(core.preferences).thenReturn(preferences)
+               whenever(core.updateChecker).thenReturn(updateChecker)
+               whenever(core.getSone(ArgumentMatchers.anyString())).thenAnswer { (localSones + remoteSones)[it.getArgument(0)].asOptional() }
+               whenever(core.getLocalSone(ArgumentMatchers.anyString())).thenAnswer { localSones[it[0]] }
+               whenever(core.getPost(ArgumentMatchers.anyString())).thenAnswer { (posts + newPosts)[it[0]].asOptional() }
+               whenever(core.getLikes(ArgumentMatchers.any<Post>())).then { postLikes[it[0]] ?: emptySet<Sone>() }
+               whenever(core.getLikes(ArgumentMatchers.any<PostReply>())).then { replyLikes[it[0]] ?: emptySet<Sone>() }
+               whenever(core.getPostReply(ArgumentMatchers.anyString())).then { replies[it[0]].asOptional() }
+               whenever(core.getAlbum(ArgumentMatchers.anyString())).then { albums[it[0]] }
+               whenever(core.getImage(ArgumentMatchers.anyString())).then { images[it[0]] }
+               whenever(core.getImage(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean())).then { images[it[0]] }
+
+               whenever(elementLoader.loadElement(ArgumentMatchers.anyString())).thenAnswer {
+                       linkedElements[it.getArgument(0)] ?: LinkedElement(it.getArgument(0), loading = true)
+               }
+
+               whenever(currentSone.options).thenReturn(DefaultSoneOptions())
+               currentSone.mock("soneId", "Sone_Id", true, 1000, idle)
+
+               whenever(freenetRequest.toadletContext).thenReturn(toadletContext)
+               whenever(freenetRequest.method).thenReturn(GET)
+               whenever(freenetRequest.httpRequest).thenReturn(httpRequest)
+
+               whenever(httpRequest.method).thenReturn("GET")
+               whenever(httpRequest.getHeader(ArgumentMatchers.anyString())).thenAnswer { requestHeaders[it.get<String>(0).toLowerCase()] }
+               whenever(httpRequest.getParam(ArgumentMatchers.anyString())).thenAnswer { requestParameters[it.getArgument(0)] ?: "" }
+               whenever(httpRequest.getParam(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())).thenAnswer { requestParameters[it.getArgument(0)] ?: it.getArgument(1) }
+               whenever(httpRequest.getParam(ArgumentMatchers.anyString(), ArgumentMatchers.isNull())).thenAnswer { requestParameters[it.getArgument(0)] }
+               whenever(httpRequest.getPart(ArgumentMatchers.anyString())).thenAnswer { requestParts[it.getArgument(0)]?.let { SimpleReadOnlyArrayBucket(it.toByteArray()) } }
+               whenever(httpRequest.getPartAsBytesFailsafe(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenAnswer { requestParts[it.getArgument(0)]?.toByteArray()?.copyOf(it.getArgument(1)) ?: ByteArray(0) }
+               whenever(httpRequest.getPartAsBytesThrowing(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenAnswer { invocation -> requestParts[invocation.getArgument(0)]?.let { it.toByteArray().let { if (it.size > invocation.getArgument<Int>(1)) throw SizeLimitExceededException() else it } } ?: throw NoSuchElementException() }
+               whenever(httpRequest.getPartAsStringFailsafe(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenAnswer { requestParts[it.getArgument(0)]?.substring(0, it.getArgument(1)) ?: "" }
+               whenever(httpRequest.getPartAsStringThrowing(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenAnswer { invocation -> requestParts[invocation.getArgument(0)]?.let { if (it.length > invocation.getArgument<Int>(1)) throw SizeLimitExceededException() else it } ?: throw NoSuchElementException() }
+               whenever(httpRequest.getIntPart(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenAnswer { invocation -> requestParts[invocation.getArgument(0)]?.toIntOrNull() ?: invocation.getArgument(1) }
+               whenever(httpRequest.isPartSet(ArgumentMatchers.anyString())).thenAnswer { it.getArgument(0) in requestParts }
+
+               whenever(currentSone.profile).thenReturn(profile)
+       }
+
+       protected fun Sone.mock(id: String, name: String, local: Boolean = false, time: Long, status: SoneStatus = idle) = apply {
+               whenever(this.id).thenReturn(id)
+               whenever(this.name).thenReturn(name)
+               whenever(isLocal).thenReturn(local)
+               whenever(this.time).thenReturn(time)
+               whenever(this.status).thenReturn(status)
+       }
+
+       protected fun unsetCurrentSone() {
+               whenever(webInterface.getCurrentSone(ArgumentMatchers.eq(toadletContext), ArgumentMatchers.anyBoolean())).thenReturn(null)
+               whenever(webInterface.getCurrentSoneWithoutCreatingSession(toadletContext)).thenReturn(null)
+               whenever(webInterface.getCurrentSoneCreatingSession(toadletContext)).thenReturn(null)
+       }
+
+       protected fun postRequest() {
+               whenever(freenetRequest.method).thenReturn(POST)
+               whenever(httpRequest.method).thenReturn("POST")
+       }
+
+       protected fun addRequestHeader(key: String, value: String) {
+               requestHeaders += key.toLowerCase() to value
+       }
+
+       protected fun addRequestParameter(key: String, value: String) {
+               requestParameters += key to value
+       }
+
+       protected fun addRequestPart(key: String, value: String) {
+               requestParts += key to value
+       }
+
+       protected fun addNotification(notification: Notification, notificationId: String? = null) {
+               notifications[notificationId ?: notification.id] = notification
+       }
+
+       protected fun addSone(sone: Sone, soneId: String? = null) {
+               remoteSones += (soneId ?: sone.id) to sone
+       }
+
+       protected fun addLocalSone(sone: Sone, id: String? = null) {
+               localSones[id ?: sone.id] = sone
+       }
+
+       protected fun addPost(post: Post, id: String? = null) {
+               posts[id ?: post.id] = post
+       }
+
+       protected fun addLikes(post: Post, vararg sones: Sone) {
+               postLikes[post] = setOf(*sones)
+       }
+
+       protected fun addLikes(reply: PostReply, vararg sones: Sone) {
+               replyLikes[reply] = setOf(*sones)
+       }
+
+       protected fun addNewPost(id: String, soneId: String, time: Long, recipientId: String? = null) =
+                       mock<Post>().apply {
+                               whenever(this.id).thenReturn(id)
+                               val sone = mock<Sone>().apply { whenever(this.id).thenReturn(soneId) }
+                               whenever(this.sone).thenReturn(sone)
+                               whenever(this.time).thenReturn(time)
+                               whenever(this.recipientId).thenReturn(recipientId.asOptional())
+                       }.also { newPosts[id] = it }
+
+       protected fun addReply(reply: PostReply, id: String? = null) {
+               replies[id ?: reply.id] = reply
+       }
+
+       protected fun addNewReply(id: String, soneId: String, postId: String, postSoneId: String) {
+               newReplies[id] = mock<PostReply>().apply {
+                       whenever(this.id).thenReturn(id)
+                       val sone = mock<Sone>().apply { whenever(this.id).thenReturn(soneId) }
+                       whenever(this.sone).thenReturn(sone)
+                       val postSone = mock<Sone>().apply { whenever(this.id).thenReturn(postSoneId) }
+                       val post = mock<Post>().apply {
+                               whenever(this.sone).thenReturn(postSone)
+                       }
+                       whenever(this.post).thenReturn(post.asOptional())
+                       whenever(this.postId).thenReturn(postId)
+               }
+       }
+
+       protected fun addLinkedElement(link: String, loading: Boolean, failed: Boolean) {
+               linkedElements[link] = LinkedElement(link, failed, loading)
+       }
+
+       protected fun addAlbum(album: Album, albumId: String? = null) {
+               albums[albumId ?: album.id] = album
+       }
+
+       protected fun addImage(image: Image, imageId: String? = null) {
+               images[imageId ?: image.id] = image
+       }
+
+       protected fun addTranslation(key: String, value: String) {
+               translations[key] = value
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/TrustAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..64a66a3
--- /dev/null
@@ -0,0 +1,39 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [TrustAjaxPage].
+ */
+class TrustAjaxPageTest : JsonPageTest("trustSone.ajax", requiresLogin = true, needsFormPassword = true, pageSupplier = ::TrustAjaxPage) {
+
+       private val sone = mock<Sone>()
+
+       @Test
+       fun `request with invalid sone results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone trust sone`() {
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).trustSone(currentSone, sone)
+       }
+
+       @Test
+       fun `request with valid sone returns positive trust value`() {
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               core.preferences.positiveTrust = 31
+               assertThatJsonIsSuccessful()
+               assertThat(json["trustValue"]?.asInt(), equalTo(31))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..65c5beb
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+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 [UnbookmarkAjaxPage].
+ */
+class UnbookmarkAjaxPageTest : JsonPageTest("unbookmark.ajax", requiresLogin = false, needsFormPassword = true, pageSupplier = ::UnbookmarkAjaxPage) {
+
+       @Test
+       fun `request without post id results in invalid-post-id`() {
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request with empty post id results in invalid-post-id`() {
+               addRequestParameter("post", "")
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request with invalid post id does not unbookmark anything and fails`() {
+               addRequestParameter("post", "invalid")
+               assertThat(json.isSuccess, equalTo(false))
+               verify(core, never()).unbookmarkPost(any())
+       }
+
+       @Test
+       fun `request with valid post id does not unbookmark anything but succeeds`() {
+               val post = mock<Post>()
+               addPost(post, "post-id")
+               addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               verify(core).unbookmarkPost(eq(post))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..e4b53fd
--- /dev/null
@@ -0,0 +1,33 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnfollowSoneAjaxPage].
+ */
+class UnfollowSoneAjaxPageTest : JsonPageTest("unfollowSone.ajax", pageSupplier = ::UnfollowSoneAjaxPage) {
+
+       @Test
+       fun `request without sone returns invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with invalid sone returns invalid-sone-id`() {
+               addRequestParameter("sone", "invalid")
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone unfollows sone`() {
+               addSone(mock(), "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).unfollowSone(currentSone, "sone-id")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlikeAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..4661892
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.web.ajax
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnlikeAjaxPage].
+ */
+class UnlikeAjaxPageTest : JsonPageTest("unlike.ajax", pageSupplier = ::UnlikeAjaxPage) {
+
+       @Test
+       fun `request without type results in error`() {
+               assertThat(json.isSuccess, equalTo(false))
+       }
+
+       @Test
+       fun `request for post without id results in invalid-post-id`() {
+               addRequestParameter("type", "post")
+               assertThatJsonFailed("invalid-post-id")
+       }
+
+       @Test
+       fun `request for invalid type results in invalid-type`() {
+               addRequestParameter("type", "invalid")
+               addRequestParameter("invalid", "invalid")
+               assertThatJsonFailed("invalid-type")
+       }
+
+       @Test
+       fun `request for post with id removes id from liked posts`() {
+               addRequestParameter("type", "post")
+               addRequestParameter("post", "post-id")
+               assertThatJsonIsSuccessful()
+               verify(currentSone).removeLikedPostId("post-id")
+               verify(core).touchConfiguration()
+       }
+
+       @Test
+       fun `request for reply without id results in invalid-reply-id`() {
+               addRequestParameter("type", "reply")
+               assertThatJsonFailed("invalid-reply-id")
+       }
+
+       @Test
+       fun `request for reply with id removes id from liked replys`() {
+               addRequestParameter("type", "reply")
+               addRequestParameter("reply", "reply-id")
+               assertThatJsonIsSuccessful()
+               verify(currentSone).removeLikedReplyId("reply-id")
+               verify(core).touchConfiguration()
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..81a54e3
--- /dev/null
@@ -0,0 +1,35 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnlockSoneAjaxPage].
+ */
+class UnlockSoneAjaxPageTest : JsonPageTest("unlockSone.ajax", requiresLogin = false, pageSupplier = ::UnlockSoneAjaxPage) {
+
+       @Test
+       fun `request without sone results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with invalid sone results in invalid-sone-id`() {
+               addRequestParameter("sone", "invalid")
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone results in locked sone`() {
+               val sone = mock<Sone>()
+               addLocalSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).unlockSone(sone)
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/ajax/UntrustAjaxPageTest.kt
new file mode 100644 (file)
index 0000000..f2de48d
--- /dev/null
@@ -0,0 +1,45 @@
+package net.pterodactylus.sone.web.ajax
+
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UntrustAjaxPage].
+ */
+class UntrustAjaxPageTest : JsonPageTest("untrustSone.ajax", pageSupplier = ::UntrustAjaxPage) {
+
+       @Test
+       fun `request without sone results in invalid-sone-id`() {
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with invalid sone results in invalid-sone-id`() {
+               addRequestParameter("sone", "invalid")
+               assertThatJsonFailed("invalid-sone-id")
+       }
+
+       @Test
+       fun `request with valid sone results in sone being untrusted`() {
+               val sone = mock<Sone>()
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               verify(core).untrustSone(currentSone, sone)
+       }
+
+       @Test
+       fun `request with valid sone results in null trust value being returned`() {
+               val sone = mock<Sone>()
+               addSone(sone, "sone-id")
+               addRequestParameter("sone", "sone-id")
+               assertThatJsonIsSuccessful()
+               assertThat(json["trustValue"], nullValue())
+       }
+
+}
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..142bf81
--- /dev/null
@@ -0,0 +1,49 @@
+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 org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [AboutPage].
+ */
+class AboutPageTest: WebPageTest({ template, webInterface -> AboutPage(template, webInterface, PluginVersion(version), PluginYear(year), PluginHomepage(homepage)) }) {
+
+       companion object {
+               private const val version = "0.1.2"
+               private const val year = 1234
+               private const val homepage = "home://page"
+       }
+
+       @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..a805843
--- /dev/null
@@ -0,0 +1,54 @@
+package net.pterodactylus.sone.web.pages
+
+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(::BookmarkPage) {
+
+       @Test
+       fun `path is set correctly`() {
+               assertThat(page.path, equalTo("bookmark.html"))
+       }
+
+       @Test
+       fun `get request does not bookmark anything and does not redirect`() {
+               verifyNoRedirect {
+                       verify(core, never()).bookmarkPost(any())
+               }
+       }
+
+       private fun setupBookmarkRequest() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return-page.html")
+               addHttpRequestPart("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..e853199
--- /dev/null
@@ -0,0 +1,59 @@
+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.utils.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(::BookmarksPage) {
+
+       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`() {
+               verifyNoRedirect {
+                       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)
+               verifyNoRedirect {
+                       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..5cc463a
--- /dev/null
@@ -0,0 +1,99 @@
+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.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(::CreateAlbumPage) {
+
+       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`() {
+               setMethod(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`() {
+               setMethod(POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestPart("name", "new name")
+               addHttpRequestPart("description", "new description")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("name", "new name")
+               addHttpRequestPart("description", "new description")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `empty album title redirects to error page`() {
+               setMethod(POST)
+               whenever(newAlbum.modify().update()).thenThrow(AlbumTitleMustNotBeEmpty::class.java)
+               addHttpRequestPart("name", "new name")
+               addHttpRequestPart("description", "new description")
+               verifyRedirect("emptyAlbumTitle.html")
+       }
+
+       @Test
+       fun `album description is filtered`() {
+               setMethod(POST)
+               addHttpRequestPart("name", "new name")
+               addHttpRequestPart("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..3b1cc8c
--- /dev/null
@@ -0,0 +1,91 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Optional.absent
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.utils.asOptional
+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(::CreatePostPage) {
+
+       @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`() {
+               addHttpRequestPart("returnPage", "return.html")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["returnPage"], equalTo<Any>("return.html"))
+       }
+
+       @Test
+       fun `post is created correctly`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("text", "post text")
+               verifyRedirect("return.html") {
+                       verify(core).createPost(currentSone, absent(), "post text")
+               }
+       }
+
+       @Test
+       fun `creating an empty post is denied`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("text", "  ")
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["errorTextEmpty"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `a sender can be selected`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("text", "post text")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("text", "post text")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..71e83bc
--- /dev/null
@@ -0,0 +1,88 @@
+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.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(::CreateReplyPage) {
+
+       @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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("text", "new text")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("text", "new text")
+               verifyRedirect("noPermission.html")
+       }
+
+}
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..9cbf2c3
--- /dev/null
@@ -0,0 +1,145 @@
+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.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(::CreateSonePage) {
+
+       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()
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(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()
+               setMethod(POST)
+               addHttpRequestPart("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..969a133
--- /dev/null
@@ -0,0 +1,107 @@
+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.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(::DeleteAlbumPage) {
+
+       private val sone = mock<Sone>()
+       private val album = mock<Album>()
+       private val parentAlbum = mock<Album>()
+
+       @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`() {
+               whenever(core.getAlbum(anyString())).thenReturn(null)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid album ID sets album in template context`() {
+               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`() {
+               setMethod(POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request redirects to no permissions page if album is not local`() {
+               setMethod(POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with abort delete parameter set redirects to album browser`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               addHttpRequestPart("abortDelete", "true")
+               verifyRedirect("imageBrowser.html?album=album-id")
+       }
+
+       @Test
+       fun `album is deleted and page redirects to sone if parent album is root album`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               whenever(parentAlbum.isRoot).thenReturn(false)
+               whenever(sone.rootAlbum).thenReturn(mock<Album>())
+               addAlbum("album-id", album)
+               addHttpRequestPart("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..1b90c4a
--- /dev/null
@@ -0,0 +1,83 @@
+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.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(::DeleteImagePage) {
+
+       private val image = mock<Image>()
+       private val sone = mock<Sone>()
+
+       @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`() {
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with image from non-local sone redirects to no permissions page`() {
+               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`() {
+               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`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("abortDelete", "true")
+               verifyRedirect("imageBrowser.html?image=image-id")
+       }
+
+       @Test
+       fun `post request deletes image and redirects to image browser`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("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..5ccd5c2
--- /dev/null
@@ -0,0 +1,105 @@
+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.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(::DeletePostPage) {
+
+       private val post = mock<Post>()
+       private val sone = mock<Sone>()
+
+       @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`() {
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `get request with valid post sets post and return page in template context`() {
+               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`() {
+               setMethod(POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with post from non-local sone redirects to no permission page`() {
+               setMethod(POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addPost("post-id", post)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with confirmation deletes post and redirects to return page`() {
+               setMethod(POST)
+               addPost("post-id", post)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addPost("post-id", post)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("abortDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core, never()).deletePost(post)
+               }
+       }
+
+       @Test
+       fun `post request without delete or abort sets post in template context`() {
+               setMethod(POST)
+               addPost("post-id", post)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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..8f344eb
--- /dev/null
@@ -0,0 +1,78 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile
+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.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(::DeleteProfileFieldPage) {
+
+       private val profile = Profile(currentSone)
+       private val field = profile.addField("name")
+
+       @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`() {
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with invalid field name redirects to invalid page`() {
+               setMethod(POST)
+               addHttpRequestPart("field", "wrong-id")
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid field name sets field in template context`() {
+               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`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("field", field.id)
+               addHttpRequestPart("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..c332557
--- /dev/null
@@ -0,0 +1,98 @@
+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.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(::DeleteReplyPage) {
+
+       private val sone = mock<Sone>()
+       private val reply = mock<PostReply>()
+
+       @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`() {
+               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`() {
+               setMethod(POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestPart("reply", "reply-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `trying to delete a reply from a non-local sone results in no permission page`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestPart("reply", "reply-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("confirmDelete", "true")
+               verifyRedirect("return.html") {
+                       verify(core).deleteReply(reply)
+               }
+       }
+
+       @Test
+       fun `aborting deletion of reply redirects to return page`() {
+               setMethod(POST)
+               addPostReply("reply-id", reply)
+               addHttpRequestPart("reply", "reply-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..415adf6
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.web.pages
+
+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.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DeleteSonePage].
+ */
+class DeleteSonePageTest: WebPageTest(::DeleteSonePage) {
+
+       @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`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request without delete confirmation redirects to index`() {
+               setMethod(POST)
+               verifyRedirect("index.html") {
+                       verify(core, never()).deleteSone(any())
+               }
+       }
+
+       @Test
+       fun `post request with delete confirmation deletes sone and redirects to index`() {
+               setMethod(POST)
+               addHttpRequestPart("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..41f47aa
--- /dev/null
@@ -0,0 +1,66 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.util.notify.Notification
+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.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [DismissNotificationPage].
+ */
+class DismissNotificationPageTest: WebPageTest(::DismissNotificationPage) {
+
+       private val notification = mock<Notification>()
+
+       @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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html")
+       }
+
+       @Test
+       fun `get request with non-dismissible notification never dismisses the notification but redirects to return page`() {
+               setMethod(POST)
+               addNotification("notification-id", notification)
+               addHttpRequestPart("notification", "notification-id")
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(notification, never()).dismiss()
+               }
+       }
+
+       @Test
+       fun `post request with dismissible notification dismisses the notification and redirects to return page`() {
+               setMethod(POST)
+               whenever(notification.isDismissable).thenReturn(true)
+               addNotification("notification-id", notification)
+               addHttpRequestPart("notification", "notification-id")
+               addHttpRequestPart("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..3261df6
--- /dev/null
@@ -0,0 +1,57 @@
+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.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(::DistrustPage) {
+
+       @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`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid sone redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html")
+       }
+
+       @Test
+       fun `post request with valid sone distrusts sone and redirects to return page`() {
+               setMethod(POST)
+               val remoteSone = mock<Sone>()
+               addSone("remote-sone-id", remoteSone)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..b4328c6
--- /dev/null
@@ -0,0 +1,123 @@
+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.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(::EditAlbumPage) {
+
+       private val album = mock<Album>()
+       private val parentAlbum = mock<Album>()
+       private val modifier = mockBuilder<Modifier>()
+       private val sone = mock<Sone>()
+
+       @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`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid album redirects to invalid page`() {
+               setMethod(POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with album of non-local sone redirects to no permissions page`() {
+               setMethod(POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with move left requested moves album to the left and redirects to album browser`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addAlbum("album-id", album)
+               addHttpRequestPart("album", "album-id")
+               addHttpRequestPart("title", "title")
+               addHttpRequestPart("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..f6eb02e
--- /dev/null
@@ -0,0 +1,148 @@
+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.Image.Modifier.ImageTitleMustNotBeEmpty
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.test.doThrow
+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.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditImagePage].
+ */
+class EditImagePageTest: WebPageTest(::EditImagePage) {
+
+       private val image = mock<Image>()
+       private val modifier = mockBuilder<Modifier>()
+       private val sone = mock<Sone>()
+       private val album = mock<Album>()
+
+       @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 `page returns correct path`() {
+               assertThat(page.path, equalTo("editImage.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.EditImage.Title")).thenReturn("edit image page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("edit image page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `post request with invalid image redirects to invalid page`() {
+               setMethod(POST)
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `post request with valid image from non-local sone redirects to no permission page`() {
+               setMethod(POST)
+               whenever(sone.isLocal).thenReturn(false)
+               addImage("image-id", image)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("title", "   ")
+               whenever(modifier.update()).doThrow<ImageTitleMustNotBeEmpty>()
+               verifyRedirect("emptyImageTitle.html") {
+                       verify(core, never()).touchConfiguration()
+               }
+       }
+
+       @Test
+       fun `post request with valid image title and description modifies image and redirects to reutrn page`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("title", "Title")
+               addHttpRequestPart("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 return page`() {
+               setMethod(POST)
+               addImage("image-id", image)
+               addHttpRequestPart("image", "image-id")
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("title", "Title")
+               addHttpRequestHeader("Host", "www.te.st")
+               addHttpRequestPart("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..b4acc7e
--- /dev/null
@@ -0,0 +1,96 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Profile
+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.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [EditProfileFieldPage].
+ */
+class EditProfileFieldPageTest: WebPageTest(::EditProfileFieldPage) {
+
+       private val profile = Profile(currentSone)
+       private val field = profile.addField("Name")
+
+       @Before
+       fun setupProfile() {
+               whenever(currentSone.profile).thenReturn(profile)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("editProfileField.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.EditProfileField.Title")).thenReturn("edit profile field title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("edit profile field title"))
+       }
+
+       @Test
+       fun `get request with invalid field redirects to invalid page`() {
+               verifyRedirect("invalid.html")
+       }
+
+       @Test
+       fun `get request with valid field stores field in template context`() {
+               addHttpRequestParameter("field", field.id)
+               page.processTemplate(freenetRequest, templateContext)
+               assertThat(templateContext["field"], equalTo<Any>(field))
+       }
+
+       @Test
+       fun `post request with cancel set redirects to profile edit page`() {
+               setMethod(POST)
+               addHttpRequestPart("field", field.id)
+               addHttpRequestPart("cancel", "true")
+               verifyRedirect("editProfile.html#profile-fields")
+       }
+
+       @Test
+       fun `post request with new name renames field and redirects to profile edit page`() {
+               setMethod(POST)
+               addHttpRequestPart("field", field.id)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("field", field.id)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               profile.addField("New Name")
+               addHttpRequestPart("field", field.id)
+               addHttpRequestPart("name", "New Name")
+               page.processTemplate(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..f76786a
--- /dev/null
@@ -0,0 +1,220 @@
+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.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(::EditProfilePage) {
+
+       private val profile = Profile(currentSone)
+       private val firstField = profile.addField("First Field")
+       private val secondField = profile.addField("Second Field")
+
+       @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 `page returns correct path`() {
+           assertThat(page.path, equalTo("editProfile.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           whenever(l10n.getString("Page.EditProfile.Title")).thenReturn("edit profile page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("edit profile page title"))
+       }
+
+       @Test
+       fun `get request stores fields of current sone’s profile in template context`() {
+               page.processTemplate(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`() {
+               setMethod(POST)
+               page.processTemplate(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) {
+               setMethod(POST)
+               addHttpRequestPart("save-profile", "true")
+               addHttpRequestPart(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`() {
+               setMethod(POST)
+               profile.addField("new-field")
+               addHttpRequestPart("add-field", "true")
+               addHttpRequestPart("field-name", "new-field")
+               page.processTemplate(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`() {
+               setMethod(POST)
+               addHttpRequestPart("add-field", "true")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("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 does not redirect`() {
+               setMethod(POST)
+               addHttpRequestPart("move-up-field-foo", "true")
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `moving a field down moves the field down and redirects to the edit profile page`() {
+               setMethod(POST)
+               addHttpRequestPart("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 does not redirect`() {
+               setMethod(POST)
+               addHttpRequestPart("move-down-field-foo", "true")
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `editing a field redirects to the edit profile page`() {
+               setMethod(POST)
+               addHttpRequestPart("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..21964aa
--- /dev/null
@@ -0,0 +1,83 @@
+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.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.ArgumentMatchers.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [FollowSonePage].
+ */
+class FollowSonePageTest: WebPageTest(::FollowSonePage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("followSone.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           whenever(l10n.getString("Page.FollowSone.Title")).thenReturn("follow sone page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("follow sone page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               page.processTemplate(freenetRequest, templateContext)
+       }
+
+       @Test
+       fun `a single sone can be followed`() {
+               setMethod(POST)
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               addHttpRequestPart("sone", "sone-id")
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core).followSone(currentSone, "sone-id")
+                       verify(core).markSoneKnown(sone)
+               }
+       }
+
+       @Test
+       fun `multiple sones can be followed`() {
+               setMethod(POST)
+               val firstSone = mock<Sone>()
+               addSone("sone-id1", firstSone)
+               val secondSone = mock<Sone>()
+               addSone("sone-id2", secondSone)
+               addHttpRequestPart("sone", "sone-id1,sone-id2")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("sone", "sone-id")
+               addHttpRequestPart("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..b5efeac
--- /dev/null
@@ -0,0 +1,62 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.TemporaryImage
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+
+/**
+ * Unit test for [GetImagePage].
+ */
+class GetImagePageTest {
+
+       private val scaffolding = WebPageTest()
+       private val page = GetImagePage(scaffolding.webInterface)
+       private val freenetRequest = scaffolding.freenetRequest
+       private val response = scaffolding.response
+       private val responseContent = scaffolding.responseContent
+
+       @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(response.statusText, equalTo("Not found."))
+               assertThat(response.contentType, equalTo("text/html; charset=utf-8"))
+               assertThat(responseContent.toByteArray(), 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)
+               }
+               scaffolding.addHttpRequestParameter("image", "temp-id")
+               scaffolding.addTemporaryImage("temp-id", image)
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.contentType, equalTo("image/test"))
+               assertThat(responseContent.toByteArray(), equalTo(ByteArray(5, Int::toByte)))
+               println(response.headers.map { it.name to it.iterator().asSequence().toList() })
+               assertThat(response.headers.map { it.name to it.iterator().asSequence().toList() }, contains(
+                               "Content-Disposition" to listOf("attachment; filename=temp-id.test")
+               ))
+       }
+
+}
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..4d0b2c7
--- /dev/null
@@ -0,0 +1,136 @@
+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 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(::ImageBrowserPage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("imageBrowser.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.ImageBrowser.Title")).thenReturn("image browser page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("image browser page title"))
+       }
+
+       @Test
+       fun `get request with album sets album and page in template context`() {
+               val album = mock<Album>()
+               addAlbum("album-id", album)
+               addHttpRequestParameter("album", "album-id")
+               addHttpRequestParameter("page", "5")
+               verifyNoRedirect {
+                       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`() {
+               val image = mock<Image>()
+               addImage("image-id", image)
+               addHttpRequestParameter("image", "image-id")
+               verifyNoRedirect {
+                       assertThat(templateContext["imageRequested"], equalTo<Any>(true))
+                       assertThat(templateContext["image"], equalTo<Any>(image))
+               }
+       }
+
+       @Test
+       fun `get request with sone sets sone in template context`() {
+               val sone = mock<Sone>()
+               addSone("sone-id", sone)
+               addHttpRequestParameter("sone", "sone-id")
+               verifyNoRedirect {
+                       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`() {
+               val firstSone = createSone("first album", "second album")
+               addSone("sone1", firstSone)
+               val secondSone = createSone("third album", "fourth album")
+               addSone("sone2", secondSone)
+               addHttpRequestParameter("mode", "gallery")
+               verifyNoRedirect {
+                       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]
+                       ))
+               }
+       }
+
+       @Test
+       fun `get request for gallery can show second page`() {
+               core.preferences.imagesPerPage = 2
+               val firstSone = createSone("first album", "second album")
+               addSone("sone1", firstSone)
+               val secondSone = createSone("third album", "fourth album")
+               addSone("sone2", secondSone)
+               addHttpRequestParameter("mode", "gallery")
+               addHttpRequestParameter("page", "1")
+               verifyNoRedirect {
+                       assertThat(templateContext["galleryRequested"], equalTo<Any>(true))
+                       @Suppress("UNCHECKED_CAST")
+                       assertThat(templateContext["albums"] as Iterable<Album>, contains(
+                                       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`() {
+               verifyNoRedirect {
+                       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..b51627d
--- /dev/null
@@ -0,0 +1,154 @@
+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.utils.Pagination
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.emptyIterable
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+
+/**
+ * Unit test for [IndexPage].
+ */
+class IndexPageTest: WebPageTest({ template, webInterface -> IndexPage(template, webInterface, postVisibilityFilter) }) {
+
+       companion object {
+               private val postVisibilityFilter = mock<PostVisibilityFilter>()
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("index.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.Index.Title")).thenReturn("index page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("index page title"))
+       }
+
+       @Before
+       fun setupPostVisibilityFilter() {
+               whenever(postVisibilityFilter.isVisible(ArgumentMatchers.eq(currentSone))).thenReturn(Predicate<Post> { true })
+       }
+
+       @Before
+       fun setupCurrentSone() {
+               whenever(currentSone.id).thenReturn("current")
+       }
+
+       @Before
+       fun setupDirectedPosts() {
+               whenever(core.getDirectedPosts("current")).thenReturn(emptyList())
+       }
+
+       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)
+               page.processTemplate(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)
+               whenever(core.getDirectedPosts("current")).thenReturn(listOf(notFollowedPosts[0]))
+               page.processTemplate(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)
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(
+                               posts[0], followedPosts[0], posts[1], followedPosts[1], posts[2]
+               ))
+       }
+
+       @Test
+       fun `index page uses post visibility filter`() {
+               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"))
+               whenever(postVisibilityFilter.isVisible(ArgumentMatchers.eq(currentSone))).thenReturn(Predicate<Post> { (it?.time ?: 10000) < 2500 })
+               addSone("followed1", followedSone)
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat(templateContext["posts"] as Iterable<Post>, contains(
+                               posts[1], followedPosts[1], posts[2]
+               ))
+       }
+
+       @Test
+       fun `index page sets pagination correctly`() {
+               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
+               whenever(currentSone.posts).thenReturn(posts)
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(
+                               posts[0], posts[1], posts[2]
+               ))
+       }
+
+       @Test
+       fun `index page sets page correctly`() {
+               val posts = listOf(createPost(3000), createPost(2000), createPost(1000))
+               whenever(currentSone.posts).thenReturn(posts)
+               core.preferences.postsPerPage = 1
+               addHttpRequestParameter("page", "2")
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               assertThat((templateContext["pagination"] as Pagination<Post>).page, equalTo(2))
+       }
+
+       @Test
+       fun `index page without posts sets correct pagination`() {
+               core.preferences.postsPerPage = 1
+               page.processTemplate(freenetRequest, templateContext)
+               @Suppress("UNCHECKED_CAST")
+               (templateContext["pagination"] as Pagination<Post>).let { pagination ->
+                       assertThat(pagination.items, emptyIterable())
+               }
+       }
+
+}
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..70a5eea
--- /dev/null
@@ -0,0 +1,245 @@
+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.utils.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 [KnownSonesPage].
+ */
+class KnownSonesPageTest: WebPageTest(::KnownSonesPage) {
+
+       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(this.isLocal).thenReturn(local)
+               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()
+               ))
+       }
+
+       private fun verifyStoredFields(sort: String, order: String, filter: String) {
+               assertThat(templateContext["sort"], equalTo<Any>(sort))
+               assertThat(templateContext["order"], equalTo<Any>(order))
+               assertThat(templateContext["filter"], equalTo<Any>(filter))
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("knownSones.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               whenever(l10n.getString("Page.KnownSones.Title")).thenReturn("known sones page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("known sones page title"))
+       }
+
+       @Test
+       fun `default known sones are sorted newest first`() {
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 2, 1, 0)
+                       verifyStoredFields("activity", "desc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by oldest first`() {
+               addHttpRequestParameter("order", "asc")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(0, 1, 2, 3)
+                       verifyStoredFields("activity", "asc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by posts, most posts first`() {
+               addHttpRequestParameter("sort", "posts")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(0, 2, 1, 3)
+                       verifyStoredFields("posts", "desc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by posts, least posts first`() {
+               addHttpRequestParameter("sort", "posts")
+               addHttpRequestParameter("order", "asc")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 1, 2, 0)
+                       verifyStoredFields("posts", "asc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by images, most images first`() {
+               addHttpRequestParameter("sort", "images")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(1, 0, 2, 3)
+                       verifyStoredFields("images", "desc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by images, least images first`() {
+               addHttpRequestParameter("sort", "images")
+               addHttpRequestParameter("order", "asc")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 2, 0, 1)
+                       verifyStoredFields("images", "asc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by nice name, ascending`() {
+               addHttpRequestParameter("sort", "name")
+               addHttpRequestParameter("order", "asc")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 1, 0, 2)
+                       verifyStoredFields("name", "asc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be sorted by nice name, descending`() {
+               addHttpRequestParameter("sort", "name")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(2, 0, 1, 3)
+                       verifyStoredFields("name", "desc", "")
+               }
+       }
+
+       @Test
+       fun `known sones can be filtered by local sones`() {
+               addHttpRequestParameter("filter", "own")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(2, 0)
+                       verifyStoredFields("activity", "desc", "own")
+               }
+       }
+
+       @Test
+       fun `known sones can be filtered by non-local sones`() {
+               addHttpRequestParameter("filter", "not-own")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 1)
+                       verifyStoredFields("activity", "desc", "not-own")
+               }
+       }
+
+       @Test
+       fun `known sones can be filtered by new sones`() {
+               addHttpRequestParameter("filter", "new")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(1, 0)
+                       verifyStoredFields("activity", "desc", "new")
+               }
+       }
+
+       @Test
+       fun `known sones can be filtered by known sones`() {
+               addHttpRequestParameter("filter", "not-new")
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 2)
+                       verifyStoredFields("activity", "desc", "not-new")
+               }
+       }
+
+       @Test
+       fun `known sones can be filtered by followed sones`() {
+               addHttpRequestParameter("filter", "followed")
+               listOf("sone1", "sone3").forEach { whenever(currentSone.hasFriend(it)).thenReturn(true) }
+               verifyNoRedirect {
+                       verifySonesAreInOrder(2, 1)
+                       verifyStoredFields("activity", "desc", "followed")
+               }
+       }
+
+       @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) }
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 0)
+                       verifyStoredFields("activity", "desc", "not-followed")
+               }
+       }
+
+       @Test
+       fun `known sones can not be filtered by followed sones if there is no current sone`() {
+               addHttpRequestParameter("filter", "followed")
+               unsetCurrentSone()
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 2, 1, 0)
+                       verifyStoredFields("activity", "desc", "followed")
+               }
+       }
+
+       @Test
+       fun `known sones can not be filtered by not-followed sones if there is no current sone`() {
+               addHttpRequestParameter("filter", "not-followed")
+               unsetCurrentSone()
+               verifyNoRedirect {
+                       verifySonesAreInOrder(3, 2, 1, 0)
+                       verifyStoredFields("activity", "desc", "not-followed")
+               }
+       }
+
+       @Test
+       fun `pagination is set in template context`() {
+               verifyNoRedirect {
+                       @Suppress("UNCHECKED_CAST")
+                       assertThat((templateContext["pagination"] as Pagination<Sone>).items, contains(*listOf(3, 2, 1, 0).map { sones[it] }.toTypedArray()))
+               }
+       }
+
+}
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..9a08737
--- /dev/null
@@ -0,0 +1,68 @@
+package net.pterodactylus.sone.web.pages
+
+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
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+/**
+ * Unit test for [LikePage].
+ */
+class LikePageTest: WebPageTest(::LikePage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("like.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           addTranslation("Page.Like.Title", "like page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("like page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               verifyNoRedirect {}
+       }
+
+       @Test
+       fun `post request with post id likes post and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("type", "post")
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("type", "reply")
+               addHttpRequestPart("reply", "reply-id")
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(currentSone).addLikedReplyId("reply-id")
+               }
+       }
+
+       @Test
+       fun `post request with invalid likes redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("type", "foo")
+               addHttpRequestPart("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..2a9b4ed
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.web.pages
+
+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.ArgumentMatchers.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [LockSonePage].
+ */
+class LockSonePageTest: WebPageTest(::LockSonePage) {
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("lockSone.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+           assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           addTranslation("Page.LockSone.Title", "lock Sone page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("lock Sone page title"))
+       }
+
+       @Test
+       fun `locking an invalid local sone redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("sone", "sone-id")
+               val sone = mock<Sone>()
+               addLocalSone("sone-id", sone)
+               addHttpRequestPart("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..1b409b7
--- /dev/null
@@ -0,0 +1,141 @@
+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.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(::LoginPage) {
+
+       private val sones = listOf(createSone("Sone", "Test"), createSone("Test"), createSone("Sone"))
+
+       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`() {
+               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`() {
+               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`() {
+               setMethod(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`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("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..9dbc82a
--- /dev/null
@@ -0,0 +1,69 @@
+package net.pterodactylus.sone.web.pages
+
+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(::LogoutPage) {
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("logout.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Logout.Title", "logout page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("logout page title"))
+       }
+
+       @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..d9a132a
--- /dev/null
@@ -0,0 +1,86 @@
+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.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 [MarkAsKnownPage].
+ */
+class MarkAsKnownPageTest: WebPageTest(::MarkAsKnownPage) {
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("markAsKnown.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.MarkAsKnown.Title", "mark as known page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("mark as known page title"))
+       }
+
+       @Test
+       fun `posts can be marked as known`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("type", "post")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("type", "reply")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("type", "sone")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("type", "foo")
+               verifyRedirect("invalid.html")
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/NewPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/NewPageTest.kt
new file mode 100644 (file)
index 0000000..96427e7
--- /dev/null
@@ -0,0 +1,84 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.junit.Before
+import org.junit.Test
+import java.util.Arrays.asList
+
+/**
+ * Unit test for [NewPage].
+ */
+class NewPageTest: WebPageTest(::NewPage) {
+
+       @Before
+       fun setupNumberOfPostsPerPage() {
+               webInterface.core.preferences.postsPerPage = 5
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("new.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.New.Title", "new page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("new page title"))
+       }
+
+       @Test
+       fun `posts are not duplicated when they come from both new posts and new replies notifications`() {
+               val extraPost = mock<Post>().withTime(2000)
+               val posts = asList(mock<Post>().withTime(1000), mock<Post>().withTime(3000))
+               val postReplies = asList(mock<PostReply>(), mock<PostReply>())
+               whenever(postReplies[0].post).thenReturn(posts[0].asOptional())
+               whenever(postReplies[1].post).thenReturn(extraPost.asOptional())
+               whenever(webInterface.getNewPosts(currentSone)).thenReturn(posts)
+               whenever(webInterface.getNewReplies(currentSone)).thenReturn(postReplies)
+
+               verifyNoRedirect {
+                       val renderedPosts = templateContext.get<List<Post>>("posts", List::class.java)
+                       assertThat(renderedPosts, containsInAnyOrder(posts[1], extraPost, posts[0]))
+               }
+       }
+
+       private fun Post.withTime(time: Long) = apply { whenever(this.time).thenReturn(time) }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `posts are paginated properly`() {
+               webInterface.core.preferences.postsPerPage = 2
+               val posts = listOf(mock<Post>().withTime(2000), mock<Post>().withTime(3000), mock<Post>().withTime(1000))
+               whenever(webInterface.getNewPosts(currentSone)).thenReturn(posts)
+               verifyNoRedirect {
+                       assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(posts[1], posts[0]))
+               }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `posts are paginated properly on second page`() {
+               webInterface.core.preferences.postsPerPage = 2
+               addHttpRequestParameter("page", "1")
+               val posts = listOf(mock<Post>().withTime(2000), mock<Post>().withTime(3000), mock<Post>().withTime(1000))
+               whenever(webInterface.getNewPosts(currentSone)).thenReturn(posts)
+               verifyNoRedirect {
+                       assertThat((templateContext["pagination"] as Pagination<Post>).items, contains(posts[2]))
+               }
+       }
+
+}
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..4e4c02e
--- /dev/null
@@ -0,0 +1,379 @@
+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.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.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(::OptionsPage) {
+
+       @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 `page returns correct path`() {
+               assertThat(page.path, equalTo("options.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Options.Title", "options page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("options page title"))
+       }
+
+       @Test
+       fun `get request stores all preferences in the template context`() {
+               verifyNoRedirect {
+                       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`() {
+               unsetCurrentSone()
+               verifyNoRedirect {
+                       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) {
+               setMethod(POST)
+               addHttpRequestPart("show-custom-avatars", "ALWAYS")
+               addHttpRequestPart("load-linked-images", "ALWAYS")
+               setValue?.also { addHttpRequestPart(option, it.toString()) }
+               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 `auto-follow option can be unset`() {
+               verifyThatOptionCanBeSet("auto-follow", null, false) { 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 sone notification option can be unset`() {
+               verifyThatOptionCanBeSet("" +
+                               "", null, false) { 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 post notification option can be unset`() {
+               verifyThatOptionCanBeSet("show-notification-new-posts", null, false) { 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 `show new reply notification option can be unset`() {
+               verifyThatOptionCanBeSet("show-notification-new-replies", null, false) { 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 `enable sone insert notifications option can be unset`() {
+               verifyThatOptionCanBeSet("enable-sone-insert-notifications", null, false) { 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()
+               setMethod(POST)
+               addHttpRequestPart(name, value)
+               verifyNoRedirect {
+                       assertThat(templateContext["fieldErrors"] as Iterable<*>, hasItem(name))
+               }
+       }
+
+       private fun <T> verifyThatPreferencesCanBeSet(name: String, setValue: String?, expectedValue: T, getter: () -> T) {
+               unsetCurrentSone()
+               setMethod(POST)
+               setValue?.also { addHttpRequestPart(name, it) }
+               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..7ebb052
--- /dev/null
@@ -0,0 +1,55 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.web.page.FreenetRequest
+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 {
+
+       @Rule @JvmField val tempFolder = TemporaryFolder()
+       private val folder by lazy { tempFolder.newFolder()!! }
+       private val page by lazy { ReloadingPage<FreenetRequest>("/prefix/", folder.path, "text/plain") }
+       private val webPageTest = WebPageTest()
+       private val freenetRequest = webPageTest.freenetRequest
+       private val responseBytes = webPageTest.responseContent
+       private val response = webPageTest.response
+
+       @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`() {
+               webPageTest.request("/prefix/path/file.txt")
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(404))
+               assertThat(response.statusText, equalTo("Not found"))
+       }
+
+       @Test
+       fun `requesting valid file results in 200 and delivers file`() {
+               Files.write(Paths.get(folder.path, "file.txt"), listOf("Hello", "World"), UTF_8)
+               webPageTest.request("/prefix/path/file.txt")
+               page.handleRequest(freenetRequest, response)
+               assertThat(response.statusCode, equalTo(200))
+               assertThat(response.statusText, equalTo("OK"))
+               assertThat(response.contentType, equalTo("text/plain"))
+               assertThat(responseBytes.toByteArray(), 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..e8ec10a
--- /dev/null
@@ -0,0 +1,88 @@
+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.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(::RescuePage) {
+
+       private val soneRescuer = mock<SoneRescuer>()
+
+       @Before
+       fun setupSoneRescuer() {
+               whenever(core.getSoneRescuer(currentSone)).thenReturn(soneRescuer)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("rescue.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+               assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Rescue.Title", "rescue page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("rescue page title"))
+       }
+
+       @Test
+       fun `get request sets rescuer in template context`() {
+               verifyNoRedirect {
+                       assertThat(templateContext["soneRescuer"], equalTo<Any>(soneRescuer))
+               }
+       }
+
+       @Test
+       fun `post request redirects to rescue page`() {
+               setMethod(POST)
+               verifyRedirect("rescue.html")
+       }
+
+       @Test
+       fun `post request with fetch and invalid edition starts next fetch`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("fetch", "true")
+               addHttpRequestPart("edition", "123")
+               verifyRedirect("rescue.html") {
+                       verify(soneRescuer).setEdition(123L)
+                       verify(soneRescuer).startNextFetch()
+               }
+       }
+
+       @Test
+       fun `post request with negative edition will not set edition`() {
+               setMethod(POST)
+               addHttpRequestPart("fetch", "true")
+               addHttpRequestPart("edition", "-123")
+               verifyRedirect("rescue.html") {
+                       verify(soneRescuer, never()).setEdition(anyLong())
+                       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..94af5da
--- /dev/null
@@ -0,0 +1,367 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.base.Optional.absent
+import com.google.common.base.Ticker
+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.isOnPage
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asOptional
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Unit test for [SearchPage].
+ */
+class SearchPageTest: WebPageTest({ template, webInterface -> SearchPage(template, webInterface, ticker) }) {
+
+       companion object {
+               val ticker = mock<Ticker>()
+       }
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("search.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Search.Title", "search page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("search page title"))
+       }
+
+       @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")
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       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 ")
+               verifyNoRedirect {
+                       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 ")
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       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", "+")
+               verifyNoRedirect {
+                       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", "-")
+               verifyNoRedirect {
+                       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? = null, middleName: String? = null, lastName: String? = null) = 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")
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       assertThat(this["soneHits"], contains(soneWithProfileField))
+               }
+       }
+
+       @Test
+       fun `sone hits are paginated correctly`() {
+               core.preferences.postsPerPage = 2
+               val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
+                               .onEach { addSone(it.id, it) }
+               addHttpRequestParameter("query", "sone")
+               verifyNoRedirect {
+                       assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
+                       assertThat(this["soneHits"], contains(sones[0], sones[2]))
+               }
+       }
+
+       @Test
+       fun `sone hits page 2 is shown correctly`() {
+               core.preferences.postsPerPage = 2
+               val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
+                               .onEach { addSone(it.id, it) }
+               addHttpRequestParameter("query", "sone")
+               addHttpRequestParameter("sonePage", "1")
+               verifyNoRedirect {
+                       assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
+                       assertThat(this["soneHits"], contains(sones[3]))
+               }
+       }
+
+       @Test
+       fun `post hits are paginated correctly`() {
+               core.preferences.postsPerPage = 2
+               val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
+               addHttpRequestParameter("query", "sone")
+               verifyNoRedirect {
+                       assertThat(this["postPagination"], isOnPage(0).hasPages(2))
+                       assertThat(this["postHits"], contains(sones[0], sones[2]))
+               }
+       }
+
+       @Test
+       fun `post hits page 2 is shown correctly`() {
+               core.preferences.postsPerPage = 2
+               val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
+               addHttpRequestParameter("query", "sone")
+               addHttpRequestParameter("postPage", "1")
+               verifyNoRedirect {
+                       assertThat(this["postPagination"], isOnPage(1).hasPages(2))
+                       assertThat(this["postHits"], contains(sones[3]))
+               }
+       }
+
+       @Test
+       fun `post search results are cached`() {
+               val post = createPost("with-match", "text")
+               val callCounter = AtomicInteger()
+               whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
+               val sone = createSoneWithPost(post)
+               addSone("sone", sone)
+               addHttpRequestParameter("query", "text")
+               verifyNoRedirect {
+                       assertThat(this["postHits"], contains(post))
+               }
+               verifyNoRedirect {
+                       assertThat(callCounter.get(), equalTo(1))
+               }
+       }
+
+       @Test
+       fun `post search results are cached for five minutes`() {
+               val post = createPost("with-match", "text")
+               val callCounter = AtomicInteger()
+               whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
+               val sone = createSoneWithPost(post)
+               addSone("sone", sone)
+               addHttpRequestParameter("query", "text")
+               verifyNoRedirect {
+                       assertThat(this["postHits"], contains(post))
+               }
+               whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1)
+               verifyNoRedirect {
+                       assertThat(callCounter.get(), equalTo(2))
+               }
+       }
+
+       @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..3cb08e6
--- /dev/null
@@ -0,0 +1,217 @@
+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.page.FreenetRequest
+import net.pterodactylus.util.notify.Notification
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.version.Version
+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
+
+/**
+ * Unit test for [SoneTemplatePage].
+ */
+class SoneTemplatePageTest : WebPageTest({ template, webInterface -> object : SoneTemplatePage("path.html", template, webInterface, true) {} }) {
+
+       @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"))
+       }
+
+       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(freenetRequest: 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")
+               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")
+               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 `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..cc77203
--- /dev/null
@@ -0,0 +1,71 @@
+package net.pterodactylus.sone.web.pages
+
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [TrustPage].
+ */
+class TrustPageTest: WebPageTest(::TrustPage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("trust.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           addTranslation("Page.Trust.Title", "title trust page")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("title trust page"))
+       }
+
+       @Test
+       fun `get method does not redirect`() {
+               verifyNoRedirect { }
+       }
+
+       @Test
+       fun `post request without sone redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).trustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request with missing sone redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..a9f784e
--- /dev/null
@@ -0,0 +1,80 @@
+package net.pterodactylus.sone.web.pages
+
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.test.capture
+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.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UnbookmarkPage].
+ */
+class UnbookmarkPageTest: WebPageTest(::UnbookmarkPage) {
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("unbookmark.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Unbookmark.Title", "unbookmark page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("unbookmark page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               verifyNoRedirect { }
+       }
+
+       @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") {
+                       val postCaptor = capture<Post>()
+                       verify(core, times(2)).unbookmarkPost(postCaptor.capture())
+                       assertThat(postCaptor.allValues, contains(notLoadedPost1, notLoadedPost2))
+               }
+       }
+
+       @Test
+       fun `post request does not unbookmark not-present post but redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unbookmarkPost(any())
+               }
+       }
+
+       @Test
+       fun `post request unbookmarks present post and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("post", "post-id")
+               addHttpRequestPart("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..01721cd
--- /dev/null
@@ -0,0 +1,56 @@
+package net.pterodactylus.sone.web.pages
+
+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 [UnfollowSonePage].
+ */
+class UnfollowSonePageTest: WebPageTest(::UnfollowSonePage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("unfollowSone.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct page title`() {
+           addTranslation("Page.UnfollowSone.Title", "unfollow page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("unfollow page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               verifyNoRedirect { }
+       }
+
+       @Test
+       fun `post request unfollows a single sone and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("sone", "sone-id")
+               verifyRedirect("return.html") {
+                       verify(core).unfollowSone(currentSone, "sone-id")
+               }
+       }
+
+       @Test
+       fun `post request unfollows two sones and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..fe00729
--- /dev/null
@@ -0,0 +1,71 @@
+package net.pterodactylus.sone.web.pages
+
+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 [UnlikePage].
+ */
+class UnlikePageTest: WebPageTest(::UnlikePage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("unlike.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Unlike.Title", "unlike page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("unlike page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               verifyNoRedirect { }
+       }
+
+       @Test
+       fun `post request does not remove any likes but redirects`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("type", "post")
+               addHttpRequestPart("post", "post-id")
+               verifyRedirect("return.html") {
+                       verify(currentSone).removeLikedPostId("post-id")
+                       verify(currentSone, never()).removeLikedReplyId(any())
+               }
+       }
+
+       @Test
+       fun `post request removes reply like and redirects`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("type", "reply")
+               addHttpRequestPart("reply", "reply-id")
+               verifyRedirect("return.html") {
+                       verify(currentSone, never()).removeLikedPostId(any())
+                       verify(currentSone).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..6d46922
--- /dev/null
@@ -0,0 +1,77 @@
+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.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 [UnlockSonePage].
+ */
+class UnlockSonePageTest: WebPageTest(::UnlockSonePage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("unlockSone.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+           assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.UnlockSone.Title", "unlock page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("unlock page title"))
+       }
+
+       @Test
+       fun `post request without sone redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `post request without invalid local sone does not unlock any sone and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("sone", "invalid-sone")
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `post request without remote sone does not unlock any sone and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("sone", "remote-sone")
+               addSone("remote-sone", mock<Sone>())
+               verifyRedirect("return.html") {
+                       verify(core, never()).unlockSone(any())
+               }
+       }
+
+       @Test
+       fun `post request with local sone unlocks sone and redirects to return page`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..9d18162
--- /dev/null
@@ -0,0 +1,73 @@
+package net.pterodactylus.sone.web.pages
+
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for [UntrustPage].
+ */
+class UntrustPageTest: WebPageTest(::UntrustPage) {
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("untrust.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+               addTranslation("Page.Untrust.Title", "untrust page title")
+           assertThat(page.getPageTitle(freenetRequest), equalTo("untrust page title"))
+       }
+
+       @Test
+       fun `get request does not redirect`() {
+               verifyNoRedirect {
+                       verify(core, never()).untrustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request without sone parameter does not untrust but redirects`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("sone", "no-sone")
+               verifyRedirect("return.html") {
+                       verify(core, never()).untrustSone(eq(currentSone), any())
+               }
+       }
+
+       @Test
+       fun `post request with valid sone parameter untrusts and redirects`() {
+               setMethod(POST)
+               addHttpRequestPart("returnPage", "return.html")
+               addHttpRequestPart("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..58e63a2
--- /dev/null
@@ -0,0 +1,118 @@
+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.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(::UploadImagePage) {
+
+       private val parentAlbum = mock<Album>().apply {
+               whenever(id).thenReturn("parent-id")
+               whenever(sone).thenReturn(currentSone)
+       }
+
+       @Test
+       fun `page returns correct path`() {
+           assertThat(page.path, equalTo("uploadImage.html"))
+       }
+
+       @Test
+       fun `page requires login`() {
+           assertThat(page.requiresLogin(), equalTo(true))
+       }
+
+       @Test
+       fun `page returns correct title`() {
+           addTranslation("Page.UploadImage.Title", "upload image page title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("upload image page title"))
+       }
+
+       @Test
+       fun `get request does not redirect or upload anything`() {
+               verifyNoRedirect {
+                       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`() {
+               setMethod(POST)
+               verifyRedirect("noPermission.html")
+       }
+
+       @Test
+       fun `post request with parent that is not the current sone results in no permission error page`() {
+               setMethod(POST)
+               addHttpRequestPart("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`() {
+               setMethod(POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestPart("parent", "parent-id")
+               addHttpRequestPart("title", " ")
+               verifyRedirect("emptyImageTitle.html")
+       }
+
+       @Test
+       fun `uploading an invalid image results in no redirect and message set in template context`() {
+               setMethod(POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestPart("parent", "parent-id")
+               addHttpRequestPart("title", "title")
+               addUploadedFile("image", "image.png", "image/png", "upload-image-invalid-image.png")
+               addTranslation("Page.UploadImage.Error.InvalidImage", "upload error - invalid image")
+               verifyNoRedirect {
+                       verify(core, never()).createTemporaryImage(any(), any())
+                       assertThat(templateContext["messages"] as String, equalTo("upload error - invalid image"))
+               }
+       }
+
+       @Test
+       fun `uploading a valid image uploads image and redirects to album browser`() {
+               setMethod(POST)
+               addAlbum("parent-id", parentAlbum)
+               addHttpRequestPart("parent", "parent-id")
+               addHttpRequestPart("title", "Title")
+               addHttpRequestPart("description", "Description @ http://localhost:8888/KSK@foo")
+               addHttpRequestHeader("Host", "localhost:8888")
+               addUploadedFile("image", "upload-image-value-image.png", "image/png", "upload-image-value-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 @ KSK@foo")
+                       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..b66f6cc
--- /dev/null
@@ -0,0 +1,100 @@
+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 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(::ViewPostPage) {
+
+       private val post = mock<Post>()
+
+       @Test
+       fun `page returns correct path`() {
+               assertThat(page.path, equalTo("viewPost.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @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`() {
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       assertThat(templateContext["post"], equalTo<Any>(post))
+                       assertThat(templateContext["raw"] as? Boolean, equalTo(true))
+               }
+       }
+
+       @Test
+       fun `page title for request without parameters is default title`() {
+               addTranslation("Page.ViewPost.Title", "view post title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("view post title"))
+       }
+
+       @Test
+       fun `page title for request with invalid post is default title`() {
+               addHttpRequestParameter("post", "invalid-post-id")
+               addTranslation("Page.ViewPost.Title", "view post title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("view post 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")
+               addTranslation("Page.ViewPost.Title", "view post title")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("This is a text that … - First M. Last - view post 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..d7ff46d
--- /dev/null
@@ -0,0 +1,207 @@
+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.isOnPage
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.Pagination
+import net.pterodactylus.sone.utils.asOptional
+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(::ViewSonePage) {
+
+       init {
+               whenever(currentSone.id).thenReturn("sone-id")
+       }
+
+       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 foreignPost3 = createPost("foreign-post3", "Third Foreign Post.", 3000, 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)
+
+       @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 `page returns correct path`() {
+               assertThat(page.path, equalTo("viewSone.html"))
+       }
+
+       @Test
+       fun `page does not require login`() {
+               assertThat(page.requiresLogin(), equalTo(false))
+       }
+
+       @Test
+       fun `get request without sone parameter stores null in template context`() {
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       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)
+               verifyNoRedirect {
+                       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")
+               verifyNoRedirect {
+                       assertThat(templateContext["posts"] as Iterable<Post>, contains(directed2, post2))
+                       assertThat(templateContext["postPagination"] as Pagination<Post>, isOnPage(0).hasPages(2))
+               }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `second page of posts is shown correctly`() {
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               addHttpRequestParameter("postPage", "1")
+               verifyNoRedirect {
+                       assertThat(templateContext["posts"] as Iterable<Post>, contains(directed1, post1))
+                       assertThat(templateContext["postPagination"] as Pagination<Post>, isOnPage(1).hasPages(2))
+               }
+       }
+
+       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)
+               val reply9 = createReply("Ninth Reply", 2050, foreignPost3)
+               whenever(currentSone.replies).thenReturn(setOf(reply1, reply2, reply3, reply4, reply5, reply6, reply7, reply8, reply9))
+               whenever(core.getReplies("post1")).thenReturn(listOf(reply3, reply5))
+               whenever(core.getReplies("post2")).thenReturn(listOf(reply4))
+               whenever(core.getReplies("foreign-post1")).thenReturn(listOf(reply8, reply1))
+               whenever(core.getReplies("foreign-post2")).thenReturn(listOf(reply2))
+               whenever(core.getReplies("post3")).thenReturn(listOf(reply6))
+               whenever(core.getReplies("foreign-post3")).thenReturn(listOf(reply9))
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               verifyNoRedirect {
+                       assertThat(templateContext["repliedPosts"] as Iterable<Post>, contains(foreignPost2, foreignPost1))
+                       assertThat(templateContext["repliedPostPagination"] as Pagination<Post>, isOnPage(0).hasPages(2))
+               }
+       }
+
+       @Test
+       @Suppress("UNCHECKED_CAST")
+       fun `second page of replies is 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)
+               val reply9 = createReply("Ninth Reply", 2050, foreignPost3)
+               whenever(currentSone.replies).thenReturn(setOf(reply1, reply2, reply3, reply4, reply5, reply6, reply7, reply8, reply9))
+               whenever(core.getReplies("post1")).thenReturn(listOf(reply3, reply5))
+               whenever(core.getReplies("post2")).thenReturn(listOf(reply4))
+               whenever(core.getReplies("foreign-post1")).thenReturn(listOf(reply8, reply1))
+               whenever(core.getReplies("foreign-post2")).thenReturn(listOf(reply2))
+               whenever(core.getReplies("post3")).thenReturn(listOf(reply6))
+               whenever(core.getReplies("foreign-post3")).thenReturn(listOf(reply9))
+               addSone("sone-id", currentSone)
+               addHttpRequestParameter("sone", "sone-id")
+               addHttpRequestParameter("repliedPostPage", "1")
+               verifyNoRedirect {
+                       assertThat(templateContext["repliedPosts"] as Iterable<Post>, contains(foreignPost3))
+                       assertThat(templateContext["repliedPostPagination"] as Pagination<Post>, isOnPage(1).hasPages(2))
+               }
+       }
+
+       @Test
+       fun `page title is default for request without parameters`() {
+               addTranslation("Page.ViewSone.Page.TitleWithoutSone", "view sone page without sone")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("view sone page without sone"))
+       }
+
+       @Test
+       fun `page title is default for request with invalid sone parameters`() {
+               addHttpRequestParameter("sone", "invalid-sone-id")
+               addTranslation("Page.ViewSone.Page.TitleWithoutSone", "view sone page without sone")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("view sone page without sone"))
+       }
+
+       @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"
+               })
+               addTranslation("Page.ViewSone.Title", "view sone page")
+               assertThat(page.getPageTitle(freenetRequest), equalTo("First M. Last - view sone page"))
+       }
+
+       @Test
+       fun `page is link-excepted`() {
+               assertThat(page.isLinkExcepted(null), equalTo(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/WebPageTest.kt
new file mode 100644 (file)
index 0000000..a1fc2de
--- /dev/null
@@ -0,0 +1,251 @@
+package net.pterodactylus.sone.web.pages
+
+import com.google.common.eventbus.EventBus
+import freenet.clients.http.ToadletContext
+import freenet.support.SimpleReadOnlyArrayBucket
+import freenet.support.api.HTTPRequest
+import freenet.support.api.HTTPUploadedFile
+import net.pterodactylus.sone.core.Preferences
+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.TemporaryImage
+import net.pterodactylus.sone.freenet.wot.OwnIdentity
+import net.pterodactylus.sone.test.deepMock
+import net.pterodactylus.sone.test.get
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.utils.asList
+import net.pterodactylus.sone.utils.asOptional
+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.Method.GET
+import net.pterodactylus.util.web.Response
+import org.junit.Assert.fail
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import java.io.ByteArrayOutputStream
+import java.net.URI
+import java.nio.charset.Charset
+import kotlin.text.Charsets.UTF_8
+
+/**
+ * Base class for web page tests.
+ */
+open class WebPageTest(pageSupplier: (Template, WebInterface) -> SoneTemplatePage = { _, _ -> mock<SoneTemplatePage>() }) {
+
+       val currentSone = mock<Sone>()
+       val template = mock<Template>()
+       val webInterface = deepMock<WebInterface>()
+       val core = webInterface.core
+       val eventBus = mock<EventBus>()
+       val preferences = Preferences(eventBus)
+       val l10n = webInterface.l10n!!
+
+       val page by lazy { pageSupplier(template, webInterface) }
+       val httpRequest = mock<HTTPRequest>()
+       val freenetRequest = mock<FreenetRequest>()
+       val templateContext = TemplateContext()
+       val toadletContext = deepMock<ToadletContext>()
+       val responseContent = ByteArrayOutputStream()
+       val response = Response(responseContent)
+
+       private val requestHeaders = mutableMapOf<String, String>()
+       private val getRequestParameters = mutableMapOf<String, MutableList<String>>()
+       private val postRequestParameters = mutableMapOf<String, ByteArray>()
+       private val uploadedFileNames = mutableMapOf<String, String>()
+       private val uploadedFileContentTypes = mutableMapOf<String, String>()
+       private val uploadedFileResources = mutableMapOf<String, String>()
+       private val ownIdentities = mutableSetOf<OwnIdentity>()
+       private val allSones = mutableMapOf<String, Sone>()
+       private val localSones = mutableMapOf<String, Sone>()
+       private val allPosts = mutableMapOf<String, Post>()
+       private val allPostReplies = mutableMapOf<String, PostReply>()
+       private val perPostReplies = mutableMapOf<String, PostReply>()
+       private val allAlbums = mutableMapOf<String, Album>()
+       private val allImages = mutableMapOf<String, Image>()
+       private val notifications = mutableMapOf<String, Notification>()
+       private val translations = mutableMapOf<String, String>()
+
+       init {
+               setupCore()
+               setupWebInterface()
+               setupHttpRequest()
+               setupFreenetRequest()
+               setupTranslations()
+       }
+
+       private fun setupCore() {
+               whenever(core.preferences).thenReturn(preferences)
+               whenever(core.identityManager.allOwnIdentities).then { ownIdentities }
+               whenever(core.sones).then { allSones.values }
+               whenever(core.getSone(anyString())).then { allSones[it[0]].asOptional() }
+               whenever(core.localSones).then { localSones.values }
+               whenever(core.getLocalSone(anyString())).then { localSones[it[0]] }
+               whenever(core.getPost(anyString())).then { allPosts[it[0]].asOptional() }
+               whenever(core.getPostReply(anyString())).then { allPostReplies[it[0]].asOptional() }
+               whenever(core.getReplies(anyString())).then { perPostReplies[it[0]].asList() }
+               whenever(core.getAlbum(anyString())).then { allAlbums[it[0]] }
+               whenever(core.getImage(anyString())).then { allImages[it[0]]}
+               whenever(core.getImage(anyString(), anyBoolean())).then { allImages[it[0]]}
+               whenever(core.getTemporaryImage(anyString())).thenReturn(null)
+       }
+
+       private fun setupWebInterface() {
+               whenever(webInterface.getCurrentSoneCreatingSession(eq(toadletContext))).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(currentSone)
+               whenever(webInterface.getCurrentSoneWithoutCreatingSession(eq(toadletContext))).thenReturn(currentSone)
+               whenever(webInterface.getNotifications(currentSone)).then { notifications.values }
+               whenever(webInterface.getNotification(anyString())).then { notifications[it[0]].asOptional() }
+       }
+
+       private fun setupHttpRequest() {
+               whenever(httpRequest.method).thenReturn("GET")
+               whenever(httpRequest.getHeader(anyString())).then { requestHeaders[it.get<String>(0).toLowerCase()] }
+               whenever(httpRequest.hasParameters()).then { getRequestParameters.isNotEmpty() }
+               whenever(httpRequest.parameterNames).then { getRequestParameters.keys }
+               whenever(httpRequest.isParameterSet(anyString())).then { it[0] in getRequestParameters }
+               whenever(httpRequest.getParam(anyString())).then { getRequestParameters[it[0]]?.firstOrNull() ?: "" }
+               whenever(httpRequest.getParam(anyString(), anyString())).then { getRequestParameters[it[0]]?.firstOrNull() ?: it[1] }
+               whenever(httpRequest.getIntParam(anyString())).then { getRequestParameters[it[0]]?.first()?.toIntOrNull() ?: 0 }
+               whenever(httpRequest.getIntParam(anyString(), anyInt())).then { getRequestParameters[it[0]]?.first()?.toIntOrNull() ?: it[1] }
+               whenever(httpRequest.getLongParam(anyString(), anyLong())).then { getRequestParameters[it[0]]?.first()?.toLongOrNull() ?: it[1] }
+               whenever(httpRequest.getMultipleParam(anyString())).then { getRequestParameters[it[0]]?.toTypedArray() ?: emptyArray<String>() }
+               whenever(httpRequest.getMultipleIntParam(anyString())).then { getRequestParameters[it[0]]?.map { it.toIntOrNull() ?: 0 } ?: emptyArray<Int>() }
+               whenever(httpRequest.isPartSet(anyString())).then { it[0] in postRequestParameters }
+               whenever(httpRequest.getPartAsStringFailsafe(anyString(), anyInt())).then { postRequestParameters[it[0]]?.decode()?.take(it[1]) ?: "" }
+               whenever(httpRequest.getUploadedFile(anyString())).then {
+                       it.get<String>(0).takeIf { it in uploadedFileNames }
+                                       ?.let { name -> UploadedFile(uploadedFileNames[name]!!, uploadedFileContentTypes[name]!!, uploadedFileResources[name]!!)
+                       }
+               }
+       }
+
+       private class UploadedFile(private val filename: String, private val contentType: String, private val resourceName: String): HTTPUploadedFile {
+               override fun getFilename() = filename
+               override fun getContentType() = contentType
+               override fun getData() = javaClass.getResourceAsStream(resourceName).readBytes().let(::SimpleReadOnlyArrayBucket)
+       }
+
+       private fun ByteArray.decode(charset: Charset = UTF_8) = String(this, charset)
+
+       private fun setupFreenetRequest() {
+               whenever(freenetRequest.method).thenReturn(GET)
+               whenever(freenetRequest.httpRequest).thenReturn(httpRequest)
+               whenever(freenetRequest.toadletContext).thenReturn(toadletContext)
+       }
+
+       private fun setupTranslations() {
+               whenever(l10n.getString(anyString())).then { translations[it[0]] ?: it[0] }
+       }
+
+       fun setMethod(method: Method) {
+               whenever(httpRequest.method).thenReturn(method.name)
+               whenever(freenetRequest.method).thenReturn(method)
+       }
+
+       fun request(uri: String) {
+               whenever(httpRequest.path).thenReturn(uri)
+               whenever(freenetRequest.uri).thenReturn(URI(uri))
+       }
+
+       fun addHttpRequestHeader(name: String, value: String) {
+               requestHeaders[name.toLowerCase()] = value
+       }
+
+       fun addHttpRequestParameter(name: String, value: String) {
+               getRequestParameters[name] = getRequestParameters.getOrElse(name) { mutableListOf<String>() }.apply { add(value) }
+       }
+
+       fun addHttpRequestPart(name: String, value: String) {
+               postRequestParameters[name] = value.toByteArray(UTF_8)
+       }
+
+       fun unsetCurrentSone() {
+               whenever(webInterface.getCurrentSoneCreatingSession(eq(toadletContext))).thenReturn(null)
+               whenever(webInterface.getCurrentSone(eq(toadletContext), anyBoolean())).thenReturn(null)
+               whenever(webInterface.getCurrentSoneWithoutCreatingSession(eq(toadletContext))).thenReturn(null)
+       }
+
+       fun addOwnIdentity(ownIdentity: OwnIdentity) {
+               ownIdentities += ownIdentity
+       }
+
+       fun addSone(id: String, sone: Sone) {
+               allSones[id] = sone
+       }
+
+       fun addLocalSone(id: String, localSone: Sone) {
+               localSones[id] = localSone
+       }
+
+       fun addPost(id: String, post: Post) {
+               allPosts[id] = post
+       }
+
+       fun addPostReply(id: String, postReply: PostReply) {
+               allPostReplies[id] = postReply
+               postReply.postId?.also { perPostReplies[it] = postReply }
+       }
+
+       fun addAlbum(id: String, album: Album) {
+               allAlbums[id] = album
+       }
+
+       fun addImage(id: String, image: Image) {
+               allImages[id] = image
+       }
+
+       fun addTranslation(key: String, value: String) {
+               translations[key] = value
+       }
+
+       fun addNotification(id: String, notification: Notification) {
+               notifications[id] = notification
+       }
+
+       fun addTemporaryImage(id: String, temporaryImage: TemporaryImage) {
+               whenever(core.getTemporaryImage(id)).thenReturn(temporaryImage)
+       }
+
+       fun addUploadedFile(name: String, filename: String, contentType: String, resource: String) {
+               uploadedFileNames[name] = filename
+               uploadedFileContentTypes[name] = contentType
+               uploadedFileResources[name] = resource
+       }
+
+       fun verifyNoRedirect(assertions: () -> Unit) {
+               var caughtException: Exception? = null
+               try {
+                       page.handleRequest(freenetRequest, templateContext)
+               } catch (e: Exception) {
+                       caughtException = e
+               }
+               caughtException?.run { throw this } ?: assertions()
+       }
+
+       fun verifyRedirect(target: String, assertions: () -> Unit = {}) {
+               try {
+                       page.handleRequest(freenetRequest, templateContext)
+                       fail()
+               } catch (re: RedirectException) {
+                       if (re.target != target) {
+                               throw re
+                       }
+                       assertions()
+               } catch (e: Exception) {
+                       throw e
+               }
+       }
+
+}
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644 (file)
index 0000000..1f0955d
--- /dev/null
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/src/test/resources/net/pterodactylus/sone/core/element-loader.html b/src/test/resources/net/pterodactylus/sone/core/element-loader.html
new file mode 100644 (file)
index 0000000..c803ff3
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+       <title>Some Nice Page Title</title>
+       <meta name="description" content="This is an example of a very nice freesite.">
+</head>
+<body>
+<h1>First Paragraph</h1>
+<p>This is the first paragraph of the very nice freesite.</p>
+</body>
+</html>
diff --git a/src/test/resources/net/pterodactylus/sone/core/element-loader2.html b/src/test/resources/net/pterodactylus/sone/core/element-loader2.html
new file mode 100644 (file)
index 0000000..7bff482
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+       <title>Some Nice Page Title</title>
+</head>
+<body>
+<h1>First Paragraph</h1>
+<p>This is the first paragraph of the very nice freesite.</p>
+</body>
+</html>
diff --git a/src/test/resources/net/pterodactylus/sone/core/element-loader3.html b/src/test/resources/net/pterodactylus/sone/core/element-loader3.html
new file mode 100644 (file)
index 0000000..d8fce37
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+       <title>Some Nice Page Title</title>
+</head>
+<body>
+<h1>First Paragraph</h1>
+<p>This is the <a href="#foo">first paragraph</a> of the very nice freesite.</p>
+</body>
+</html>
diff --git a/src/test/resources/net/pterodactylus/sone/core/element-loader4.html b/src/test/resources/net/pterodactylus/sone/core/element-loader4.html
new file mode 100644 (file)
index 0000000..b660b6e
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+       <meta name="description" content="This is an example of a very nice freesite.">
+</head>
+<body>
+<h1>First Paragraph</h1>
+<p>This is the first paragraph of the very nice freesite.</p>
+</body>
+</html>
diff --git a/src/test/resources/net/pterodactylus/sone/main/custom-version.yaml b/src/test/resources/net/pterodactylus/sone/main/custom-version.yaml
new file mode 100644 (file)
index 0000000..94517d5
--- /dev/null
@@ -0,0 +1,2 @@
+id: some-id
+nice: some-nice
diff --git a/src/test/resources/net/pterodactylus/sone/web/pages/upload-image-invalid-image.png b/src/test/resources/net/pterodactylus/sone/web/pages/upload-image-invalid-image.png
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/test/resources/net/pterodactylus/sone/web/pages/upload-image-value-image.png b/src/test/resources/net/pterodactylus/sone/web/pages/upload-image-value-image.png
new file mode 100644 (file)
index 0000000..6daa837
Binary files /dev/null and b/src/test/resources/net/pterodactylus/sone/web/pages/upload-image-value-image.png differ
diff --git a/src/test/resources/version.yaml b/src/test/resources/version.yaml
new file mode 100644 (file)
index 0000000..22023b5
--- /dev/null
@@ -0,0 +1,2 @@
+id: 43f3e1c3a0f487e37e5851a2cc72756d271c7571
+nice: 0.9.6-466-g43f3e1c
diff --git a/version.gradle b/version.gradle
new file mode 100644 (file)
index 0000000..95ae89c
--- /dev/null
@@ -0,0 +1,36 @@
+buildscript {
+    repositories {
+        mavenCentral()
+    }
+    dependencies {
+               classpath group: "org.ajoberstar", name: "gradle-git", version: "1.3.0"
+    }
+}
+
+import org.ajoberstar.grgit.Grgit
+
+task(writeVersion) << {
+       def grgit = Grgit.open(dir: project.rootDir)
+       def version = grgit.resolve.toCommit("HEAD").id
+       def niceVersion = grgit.describe()
+       grgit.close()
+
+    new File("src/generated/resources").mkdirs()
+       project.file("src/generated/resources/version.yaml").withWriter { out ->
+               out.println "id: ${version}"
+               out.println "nice: ${niceVersion}"
+       }
+}
+
+sourceSets {
+    main {
+        resources {
+            srcDirs += "src/generated/resources"
+        }
+    }
+}
+
+processResources {
+    dependsOn(writeVersion)
+}
+