From f55164499f2466ecfd977631d25525e3498ed825 Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister Date: Wed, 21 Dec 2016 14:08:08 +0100 Subject: [PATCH] Initial Commit --- .dockerignore | 3 + .editorconfig | 19 + .gitignore | 11 + .gitmodules | 7 + .travis.yml | 39 + API/Rakefile | 19 + API/bootstrap.php | 4 + API/config/system.ini | 1 + API/index.php | 13 + API/phpunit.xml | 35 + API/scripts/create-feeds.sh | 11 + API/scripts/create-system-token.php | 31 + API/scripts/create-user.sh | 33 + API/src/Access/AccessControl.php | 57 + .../AccessControl/AbstractAccessControl.php | 28 + .../AccessControl/CollectionAccessControl.php | 33 + .../AccessControl/FeedAccessControl.php | 130 ++ .../AccessTypes/AccessTypeInterface.php | 11 + API/src/Access/AccessTypes/FullAccess.php | 14 + API/src/Access/AccessTypes/NoAccess.php | 11 + API/src/Access/AccessTypes/ScopedAccess.php | 54 + API/src/Access/AccessTypes/SystemAccess.php | 11 + API/src/Backends/SearchBackend.php | 123 ++ API/src/Bootstrap/Bootstrapper.php | 73 + API/src/Builders/UriBuilder.php | 24 + .../BetaRequest/CreateBetaRequestCommand.php | 27 + API/src/Commands/CreateCollectionCommand.php | 28 + API/src/Commands/CreateFeedCommand.php | 47 + API/src/Commands/DeleteAccessTokenCommand.php | 27 + API/src/Commands/DeleteCollectionCommand.php | 27 + .../Feed/CreateFeedUploadUrlCommand.php | 36 + .../Commands/Feed/CreateInvitationCommand.php | 38 + .../Commands/Feed/DeleteInvitationCommand.php | 26 + .../Commands/Feed/SetFeedVanityCommand.php | 41 + .../Commands/Feed/UpdateFeedUserCommand.php | 37 + .../Commands/Feed/UpdateInvitationCommand.php | 27 + .../Feeds/CreateFeedPersonCommand.php | 38 + .../Feeds/DeleteFeedPersonCommand.php | 37 + API/src/Commands/Feeds/FollowFeedCommand.php | 40 + .../Commands/Feeds/UnfollowFeedCommand.php | 38 + API/src/Commands/Feeds/UpdateFeedCommand.php | 35 + API/src/Commands/File/CreateFileCommand.php | 27 + API/src/Commands/Posts/CreatePostCommand.php | 70 + API/src/Commands/Posts/DeletePostCommand.php | 39 + API/src/Commands/SaveAccessTokenCommand.php | 27 + API/src/Commands/SendVerificationCommand.php | 29 + API/src/Commands/UpdateCollectionCommand.php | 28 + API/src/Commands/User/CreateUserCommand.php | 30 + API/src/Commands/User/UpdateUserCommand.php | 27 + API/src/Commands/User/VerifyUserCommand.php | 27 + API/src/DataStore/DataStoreReader.php | 38 + API/src/DataStore/DataStoreWriter.php | 147 ++ API/src/Endpoints/AbstractEndpoint.php | 71 + API/src/Endpoints/AuthEndpoint.php | 33 + .../BetaRequest/CreateBetaRequestEndpoint.php | 34 + .../Collections/CreateCollectionEndpoint.php | 44 + .../Collections/DeleteCollectionEndpoint.php | 44 + .../Collections/GetCollectionEndpoint.php | 44 + .../Collections/UpdateCollectionEndpoint.php | 44 + API/src/Endpoints/EndpointInterface.php | 23 + .../Endpoints/Feeds/CreateFeedEndpoint.php | 45 + .../Feeds/CreateInvitationEndpoint.php | 44 + .../Endpoints/Feeds/CreateUploadEndpoint.php | 44 + .../Feeds/DeleteFeedInvitationEndpoint.php | 44 + .../Feeds/DeleteFeedUserEndpoint.php | 44 + .../Endpoints/Feeds/FollowFeedEndpoint.php | 44 + API/src/Endpoints/Feeds/GetFeedEndpoint.php | 35 + .../Feeds/GetFeedInvitationsEndpoint.php | 44 + .../Endpoints/Feeds/GetFeedUsersEndpoint.php | 34 + API/src/Endpoints/Feeds/GetFeedsEndpoint.php | 45 + .../Endpoints/Feeds/UnfollowFeedEndpoint.php | 44 + .../Endpoints/Feeds/UpdateFeedEndpoint.php | 44 + .../Feeds/UpdateFeedInvitationEndpoint.php | 44 + .../Feeds/UpdateFeedUserEndpoint.php | 44 + API/src/Endpoints/Index/GetIndexEndpoint.php | 34 + .../Endpoints/Posts/CreatePostEndpoint.php | 44 + .../Endpoints/Posts/DeletePostEndpoint.php | 44 + API/src/Endpoints/Posts/GetPostEndpoint.php | 34 + API/src/Endpoints/Posts/GetPostsEndpoint.php | 34 + .../Endpoints/Posts/UpdatePostEndpoint.php | 44 + .../Endpoints/Profiles/GetProfileEndpoint.php | 49 + .../Endpoints/Random/GetRandomEndpoint.php | 34 + API/src/Endpoints/RevokeEndpoint.php | 33 + API/src/Endpoints/SearchEndpoint.php | 43 + API/src/Endpoints/User/GetTodoEndpoint.php | 44 + .../Endpoints/User/GetUpcomingEndpoint.php | 44 + .../User/GetUserCollectionsEndpoint.php | 44 + API/src/Endpoints/User/GetUserEndpoint.php | 44 + .../Endpoints/User/GetUserFeedEndpoint.php | 44 + .../Endpoints/User/GetUserFeedsEndpoint.php | 44 + API/src/Endpoints/User/UpdateUserEndpoint.php | 44 + .../User/UpdateUserPasswordEndpoint.php | 41 + .../Endpoints/Users/CreateUserEndpoint.php | 35 + API/src/Endpoints/Verify/ResendEndpoint.php | 35 + API/src/Endpoints/Verify/VerifyEndpoint.php | 35 + .../ErrorHandlers/DevelopmentErrorHandler.php | 55 + .../ErrorHandlers/ProductionErrorHandler.php | 67 + API/src/Exceptions/AbstractException.php | 30 + API/src/Exceptions/BadRequest.php | 16 + API/src/Exceptions/Forbidden.php | 16 + API/src/Exceptions/MethodNotAllowed.php | 16 + API/src/Exceptions/NotFound.php | 16 + API/src/Exceptions/Unauthorized.php | 16 + API/src/Factories/ApplicationFactory.php | 69 + API/src/Factories/BackendFactory.php | 13 + API/src/Factories/CommandFactory.php | 198 +++ API/src/Factories/ControllerFactory.php | 618 ++++++++ API/src/Factories/EndpointFactory.php | 298 ++++ API/src/Factories/ErrorHandlerFactory.php | 21 + API/src/Factories/HandlerFactory.php | 668 ++++++++ API/src/Factories/MapperFactory.php | 35 + API/src/Factories/QueryFactory.php | 257 ++++ API/src/Factories/RouterFactory.php | 63 + API/src/Factories/ServiceFactory.php | 67 + API/src/Handlers/CommandHandler.php | 17 + .../Delete/Collection/CommandHandler.php | 33 + .../Delete/Collection/QueryHandler.php | 56 + .../Delete/Collection/RequestHandler.php | 23 + .../Delete/Feed/Invitation/CommandHandler.php | 35 + .../Delete/Feed/Invitation/QueryHandler.php | 55 + .../Delete/Feed/Invitation/RequestHandler.php | 27 + .../Delete/Feed/People/CommandHandler.php | 35 + .../Delete/Feed/People/QueryHandler.php | 61 + .../Delete/Feed/People/RequestHandler.php | 26 + .../Handlers/Delete/Post/CommandHandler.php | 35 + API/src/Handlers/Delete/Post/QueryHandler.php | 45 + .../Handlers/Delete/Post/RequestHandler.php | 23 + .../Handlers/Get/Collection/QueryHandler.php | 56 + .../Get/Collection/RequestHandler.php | 23 + .../Get/Feed/Invitations/QueryHandler.php | 64 + .../Handlers/Get/Feed/People/QueryHandler.php | 68 + .../Get/Feed/People/RequestHandler.php | 23 + .../Handlers/Get/Feed/Posts/QueryHandler.php | 77 + .../Get/Feed/Posts/RequestHandler.php | 26 + API/src/Handlers/Get/Feed/QueryHandler.php | 105 ++ API/src/Handlers/Get/Feed/RequestHandler.php | 25 + API/src/Handlers/Get/Feeds/QueryHandler.php | 53 + API/src/Handlers/Get/Index/QueryHandler.php | 21 + API/src/Handlers/Get/ListRequestHandler.php | 46 + API/src/Handlers/Get/Post/QueryHandler.php | 86 ++ API/src/Handlers/Get/Post/RequestHandler.php | 23 + API/src/Handlers/Get/Profile/QueryHandler.php | 45 + .../Handlers/Get/Profile/RequestHandler.php | 31 + API/src/Handlers/Get/Random/QueryHandler.php | 31 + API/src/Handlers/Get/Search/QueryHandler.php | 55 + .../Handlers/Get/Search/RequestHandler.php | 44 + .../Get/User/Collections/QueryHandler.php | 61 + .../Handlers/Get/User/Feed/QueryHandler.php | 55 + .../Handlers/Get/User/Feeds/QueryHandler.php | 50 + API/src/Handlers/Get/User/QueryHandler.php | 44 + .../Handlers/Get/User/Todo/QueryHandler.php | 54 + .../Get/User/Upcoming/QueryHandler.php | 54 + .../Patch/Collection/CommandHandler.php | 43 + .../Patch/Collection/QueryHandler.php | 46 + .../Patch/Collection/RequestHandler.php | 30 + .../Handlers/Patch/Feed/CommandHandler.php | 57 + .../Patch/Feed/Invitation/CommandHandler.php | 40 + .../Patch/Feed/Invitation/QueryHandler.php | 55 + .../Patch/Feed/Invitation/RequestHandler.php | 11 + API/src/Handlers/Patch/Feed/QueryHandler.php | 65 + .../Handlers/Patch/Feed/RequestHandler.php | 92 ++ .../Patch/Feed/User/CommandHandler.php | 40 + .../Handlers/Patch/Feed/User/QueryHandler.php | 61 + .../Patch/Feed/User/RequestHandler.php | 50 + .../Handlers/Patch/User/CommandHandler.php | 42 + API/src/Handlers/Patch/User/QueryHandler.php | 41 + .../Handlers/Patch/User/RequestHandler.php | 29 + API/src/Handlers/Post/Auth/CommandHandler.php | 65 + API/src/Handlers/Post/Auth/QueryHandler.php | 47 + API/src/Handlers/Post/Auth/RequestHandler.php | 69 + .../Post/BetaRequest/CommandHandler.php | 33 + .../Post/BetaRequest/QueryHandler.php | 34 + .../Post/BetaRequest/RequestHandler.php | 33 + .../Post/Collections/CommandHandler.php | 46 + .../CreateCollectionCommandHandler.php | 55 + .../Post/Collections/RequestHandler.php | 35 + .../Post/Feed/Follow/CommandHandler.php | 49 + .../Post/Feed/Follow/QueryHandler.php | 70 + .../Post/Feed/FollowRequestHandler.php | 25 + .../Post/Feed/Invitations/CommandHandler.php | 45 + .../Post/Feed/Invitations/QueryHandler.php | 86 ++ .../Post/Feed/Invitations/RequestHandler.php | 54 + .../Post/Feed/Unfollow/CommandHandler.php | 38 + .../Post/Feed/Unfollow/QueryHandler.php | 56 + .../Post/Feed/Upload/CommandHandler.php | 53 + .../Post/Feed/Upload/RequestHandler.php | 33 + .../Handlers/Post/Feeds/CommandHandler.php | 46 + .../Handlers/Post/Feeds/RequestHandler.php | 62 + .../Handlers/Post/Posts/CommandHandler.php | 61 + API/src/Handlers/Post/Posts/QueryHandler.php | 63 + .../Handlers/Post/Posts/RequestHandler.php | 125 ++ .../Handlers/Post/Revoke/CommandHandler.php | 35 + .../Handlers/Post/Users/CommandHandler.php | 61 + API/src/Handlers/Post/Users/QueryHandler.php | 66 + .../Handlers/Post/Users/RequestHandler.php | 47 + .../Handlers/Post/Verify/CommandHandler.php | 38 + API/src/Handlers/Post/Verify/QueryHandler.php | 41 + .../Handlers/Post/Verify/RequestHandler.php | 29 + .../Post/Verify/Resend/CommandHandler.php | 34 + .../Post/Verify/Resend/QueryHandler.php | 48 + .../Post/Verify/Resend/RequestHandler.php | 35 + API/src/Handlers/PostHandler.php | 41 + API/src/Handlers/PreHandler.php | 55 + API/src/Handlers/Put/User/CommandHandler.php | 33 + API/src/Handlers/Put/User/QueryHandler.php | 33 + API/src/Handlers/Put/User/RequestHandler.php | 37 + API/src/Handlers/QueryHandler.php | 17 + API/src/Handlers/RequestHandler.php | 18 + API/src/Handlers/ResponseHandler.php | 23 + API/src/Handlers/TransformationHandler.php | 20 + API/src/Locators/PostTypeLocator.php | 25 + API/src/Mappers/FeedUserMapper.php | 45 + API/src/Mappers/PostAttachmentMapper.php | 40 + API/src/Mappers/ResultsMapper.php | 27 + API/src/Mappers/SearchResultsMapper.php | 21 + API/src/Models/APIModel.php | 95 ++ API/src/Models/AuthModel.php | 71 + API/src/Models/BetaRequest/CreateModel.php | 27 + API/src/Models/Collection/CollectionModel.php | 27 + API/src/Models/Collection/CreateModel.php | 27 + API/src/Models/Collection/UpdateModel.php | 13 + API/src/Models/Feed/CreateModel.php | 58 + API/src/Models/Feed/FeedModel.php | 27 + API/src/Models/Feed/FollowModel.php | 46 + .../Models/Feed/Invitation/CreateModel.php | 73 + .../Models/Feed/Invitation/DeleteModel.php | 26 + .../Models/Feed/Invitation/UpdateModel.php | 14 + API/src/Models/Feed/People/DeleteModel.php | 27 + API/src/Models/Feed/People/ListModel.php | 24 + API/src/Models/Feed/Posts/ListModel.php | 26 + API/src/Models/Feed/UpdateModel.php | 33 + API/src/Models/Feed/UploadModel.php | 56 + API/src/Models/Feed/User/UpdateModel.php | 42 + API/src/Models/ListModel.php | 39 + API/src/Models/Post/CreateModel.php | 104 ++ API/src/Models/Post/PostModel.php | 26 + API/src/Models/Profile/ProfileModel.php | 27 + API/src/Models/SearchModel.php | 46 + API/src/Models/UpdateModelTrait.php | 39 + API/src/Models/User/CreateModel.php | 59 + API/src/Models/User/UpdateModel.php | 14 + API/src/Models/User/UpdatePasswordModel.php | 39 + API/src/Models/Verify/ResendModel.php | 42 + API/src/Models/Verify/VerifyModel.php | 27 + .../FetchBetaRequestByEmailQuery.php | 26 + API/src/Queries/Feed/FeedExistsQuery.php | 26 + API/src/Queries/Feed/FetchFeedUserQuery.php | 26 + API/src/Queries/Feed/FetchFeedVanityQuery.php | 26 + API/src/Queries/Feed/FetchInvitationQuery.php | 26 + .../Queries/Feed/FetchInvitationsQuery.php | 26 + .../Queries/Feed/FetchVanityByNameQuery.php | 26 + .../Queries/Feed/InvitationExistsQuery.php | 26 + API/src/Queries/Feeds/FetchFeedQuery.php | 32 + API/src/Queries/Feeds/FetchFeedsQuery.php | 26 + API/src/Queries/Feeds/FetchFollowerQuery.php | 28 + API/src/Queries/Feeds/FetchPeopleQuery.php | 26 + API/src/Queries/Feeds/FetchPersonQuery.php | 28 + API/src/Queries/FetchCollectionQuery.php | 28 + .../Queries/File/FetchFileByPublicIdQuery.php | 26 + .../Post/FetchPostAttachmentsQuery.php | 26 + API/src/Queries/Post/FetchPostInfoQuery.php | 26 + API/src/Queries/Posts/FetchFeedPostsQuery.php | 26 + API/src/Queries/Posts/FetchPostQuery.php | 48 + API/src/Queries/Profile/FetchProfileQuery.php | 26 + API/src/Queries/SearchQuery.php | 27 + API/src/Queries/User/FetchAuthUserQuery.php | 26 + API/src/Queries/User/FetchTodoTasksQuery.php | 27 + .../Queries/User/FetchUpcomingEventsQuery.php | 27 + .../Queries/User/FetchUserByEmailQuery.php | 27 + API/src/Queries/User/FetchUserByIdQuery.php | 27 + .../Queries/User/FetchUserByUsernameQuery.php | 26 + .../User/FetchUserCollectionsQuery.php | 27 + API/src/Queries/User/FetchUserFeedQuery.php | 26 + API/src/Queries/User/FetchUserFeedsQuery.php | 26 + .../Queries/User/FetchUserPasswordQuery.php | 23 + API/src/Queries/User/FetchUsernameQuery.php | 27 + .../User/FetchVerificationTokenByEmail.php | 27 + .../User/FetchVerificationTokenQuery.php | 27 + API/src/Queries/User/IsInvitedQuery.php | 27 + API/src/Readers/RequestTokenReader.php | 20 + API/src/Routers/EndpointRouter.php | 67 + API/src/Services/BetaRequestService.php | 49 + API/src/Services/CollectionService.php | 74 + API/src/Services/FeedService.php | 188 +++ API/src/Services/FileService.php | 43 + API/src/Services/FollowerService.php | 59 + API/src/Services/PeopleService.php | 91 ++ API/src/Services/PostService.php | 188 +++ API/src/Services/UserService.php | 172 +++ API/src/ValueObjects/AccessToken.php | 92 ++ API/src/ValueObjects/Attachment.php | 39 + API/src/ValueObjects/BsonDateTime.php | 30 + API/src/ValueObjects/CollectionId.php | 19 + API/src/ValueObjects/CollectionName.php | 34 + API/src/ValueObjects/FeedDescription.php | 35 + API/src/ValueObjects/FeedFile.php | 48 + API/src/ValueObjects/FeedId.php | 22 + API/src/ValueObjects/FeedName.php | 39 + API/src/ValueObjects/FeedVanity.php | 11 + API/src/ValueObjects/FileToken.php | 16 + API/src/ValueObjects/Hash.php | 24 + API/src/ValueObjects/Pagination.php | 52 + API/src/ValueObjects/Password.php | 35 + API/src/ValueObjects/PostBody.php | 29 + API/src/ValueObjects/PostTitle.php | 34 + API/src/ValueObjects/StringBoolean.php | 36 + API/src/ValueObjects/UploadParams.php | 46 + API/src/ValueObjects/UserId.php | 19 + API/src/ValueObjects/Username.php | 39 + API/tests/bootstrap.php | 10 + Application/.babelrc | 13 + Application/.rollup.config.js | 15 + Application/Rakefile | 32 + Application/js/_stubs/dom4.js | 18 + Application/js/_stubs/encoding.js | 11 + Application/js/_stubs/events.js | 23 + Application/js/_stubs/object.js | 7 + Application/js/app/ajax.js | 77 + Application/js/application.js | 6 + Application/js/bootstrap/browser.js | 7 + Application/js/bootstrap/elements.js | 42 + Application/js/dom/custom-events.js | 9 + Application/js/dom/environment.js | 13 + Application/js/dom/fetch.js | 18 + Application/js/dom/form.js | 17 + Application/js/dom/next-render.js | 32 + Application/js/dom/string.js | 16 + Application/js/dom/toast.js | 20 + Application/js/dom/uploader.js | 56 + Application/js/elements/ajax-button.js | 70 + Application/js/elements/ajax-form.js | 125 ++ Application/js/elements/ajax-select.js | 72 + Application/js/elements/auto-textarea.js | 45 + Application/js/elements/file-drop.js | 107 ++ Application/js/elements/file-pick.js | 31 + Application/js/elements/file-upload.js | 123 ++ Application/js/elements/follow-button.js | 91 ++ Application/js/elements/form-error.js | 36 + Application/js/elements/form-field.js | 7 + Application/js/elements/paginated-list.js | 11 + Application/js/elements/paginated-view.js | 120 ++ Application/js/elements/pagination-button.js | 62 + Application/js/elements/post-attachment.js | 89 ++ Application/js/elements/time-elements.js | 597 ++++++++ Application/js/elements/toast-message.js | 103 ++ Application/js/elements/validated-input.js | 61 + Application/js/event/signal.js | 43 + Application/polyfills/README.md | 12 + Application/polyfills/custom-elements.js | 1 + Application/polyfills/dom4.js | 2 + Application/polyfills/fetch.js | 433 ++++++ Application/polyfills/object.js | 17 + Framework/Rakefile | 20 + Framework/bootstrap.php | 5 + Framework/lib/Elasticsearch/Client.php | 1350 +++++++++++++++++ Framework/lib/Elasticsearch/ClientBuilder.php | 617 ++++++++ .../lib/Elasticsearch/Common/EmptyLogger.php | 35 + .../Exceptions/AlreadyExpiredException.php | 16 + .../Exceptions/BadMethodCallException.php | 18 + .../Exceptions/BadRequest400Exception.php | 16 + .../ClientErrorResponseException.php | 16 + .../Exceptions/Conflict409Exception.php | 16 + .../Exceptions/Curl/CouldNotConnectToHost.php | 19 + .../Curl/CouldNotResolveHostException.php | 19 + .../Curl/OperationTimeoutException.php | 19 + .../Exceptions/ElasticsearchException.php | 16 + .../Exceptions/Forbidden403Exception.php | 16 + .../Exceptions/InvalidArgumentException.php | 18 + .../Common/Exceptions/MaxRetriesException.php | 16 + .../Common/Exceptions/Missing404Exception.php | 16 + .../Exceptions/NoDocumentsToGetException.php | 16 + .../Exceptions/NoNodesAvailableException.php | 16 + .../Exceptions/NoShardAvailableException.php | 16 + .../Exceptions/RequestTimeout408Exception.php | 16 + .../Exceptions/RoutingMissingException.php | 17 + .../Common/Exceptions/RuntimeException.php | 16 + .../ScriptLangNotSupportedException.php | 16 + .../Serializer/JsonErrorException.php | 69 + .../ServerErrorResponseException.php | 16 + .../Common/Exceptions/TransportException.php | 16 + .../Exceptions/UnexpectedValueException.php | 18 + .../ConnectionPool/AbstractConnectionPool.php | 86 ++ .../ConnectionPoolInterface.php | 29 + .../Selectors/RandomSelector.php | 29 + .../Selectors/RoundRobinSelector.php | 36 + .../Selectors/SelectorInterface.php | 24 + .../Selectors/StickyRoundRobinSelector.php | 47 + .../ConnectionPool/SimpleConnectionPool.php | 34 + .../ConnectionPool/SniffingConnectionPool.php | 155 ++ .../ConnectionPool/StaticConnectionPool.php | 93 ++ .../StaticNoPingConnectionPool.php | 76 + .../Elasticsearch/Connections/Connection.php | 706 +++++++++ .../Connections/ConnectionFactory.php | 67 + .../ConnectionFactoryInterface.php | 35 + .../Connections/ConnectionInterface.php | 99 ++ .../Endpoints/AbstractEndpoint.php | 286 ++++ .../lib/Elasticsearch/Endpoints/Bulk.php | 83 + .../Endpoints/BulkEndpointInterface.php | 25 + .../Elasticsearch/Endpoints/Cat/Aliases.php | 75 + .../Endpoints/Cat/Allocation.php | 75 + .../lib/Elasticsearch/Endpoints/Cat/Count.php | 55 + .../Elasticsearch/Endpoints/Cat/Fielddata.php | 73 + .../Elasticsearch/Endpoints/Cat/Health.php | 51 + .../lib/Elasticsearch/Endpoints/Cat/Help.php | 46 + .../Elasticsearch/Endpoints/Cat/Indices.php | 59 + .../Elasticsearch/Endpoints/Cat/Master.php | 50 + .../Elasticsearch/Endpoints/Cat/NodeAttrs.php | 50 + .../lib/Elasticsearch/Endpoints/Cat/Nodes.php | 50 + .../Endpoints/Cat/PendingTasks.php | 50 + .../Elasticsearch/Endpoints/Cat/Plugins.php | 50 + .../Elasticsearch/Endpoints/Cat/Recovery.php | 56 + .../Endpoints/Cat/Repositories.php | 50 + .../Elasticsearch/Endpoints/Cat/Segments.php | 62 + .../Elasticsearch/Endpoints/Cat/Shards.php | 56 + .../Elasticsearch/Endpoints/Cat/Snapshots.php | 72 + .../lib/Elasticsearch/Endpoints/Cat/Tasks.php | 52 + .../Endpoints/Cat/ThreadPool.php | 54 + .../Elasticsearch/Endpoints/ClearScroll.php | 74 + .../Endpoints/Cluster/AllocationExplain.php | 62 + .../Endpoints/Cluster/Health.php | 59 + .../Cluster/Nodes/AbstractNodesEndpoint.php | 47 + .../Endpoints/Cluster/Nodes/HotThreads.php | 51 + .../Endpoints/Cluster/Nodes/Info.php | 77 + .../Endpoints/Cluster/Nodes/Shutdown.php | 49 + .../Endpoints/Cluster/Nodes/Stats.php | 110 ++ .../Endpoints/Cluster/PendingTasks.php | 46 + .../Endpoints/Cluster/Reroute.php | 68 + .../Endpoints/Cluster/Settings/Get.php | 48 + .../Endpoints/Cluster/Settings/Put.php | 63 + .../Elasticsearch/Endpoints/Cluster/State.php | 82 + .../Elasticsearch/Endpoints/Cluster/Stats.php | 70 + .../lib/Elasticsearch/Endpoints/Count.php | 86 ++ .../Endpoints/CountPercolate.php | 90 ++ .../lib/Elasticsearch/Endpoints/Create.php | 107 ++ .../lib/Elasticsearch/Endpoints/Delete.php | 75 + .../lib/Elasticsearch/Endpoints/Exists.php | 72 + .../lib/Elasticsearch/Endpoints/Explain.php | 99 ++ .../Elasticsearch/Endpoints/FieldStats.php | 73 + Framework/lib/Elasticsearch/Endpoints/Get.php | 113 ++ .../lib/Elasticsearch/Endpoints/Index.php | 123 ++ .../Indices/Alias/AbstractAliasEndpoint.php | 38 + .../Endpoints/Indices/Alias/Delete.php | 83 + .../Endpoints/Indices/Alias/Exists.php | 77 + .../Endpoints/Indices/Alias/Get.php | 77 + .../Endpoints/Indices/Alias/Put.php | 97 ++ .../Endpoints/Indices/Aliases/Get.php | 75 + .../Endpoints/Indices/Aliases/Update.php | 77 + .../Endpoints/Indices/Analyze.php | 79 + .../Endpoints/Indices/Cache/Clear.php | 62 + .../Endpoints/Indices/ClearCache.php | 62 + .../Elasticsearch/Endpoints/Indices/Close.php | 61 + .../Endpoints/Indices/Create.php | 77 + .../Endpoints/Indices/Delete.php | 51 + .../Endpoints/Indices/Exists.php | 60 + .../Endpoints/Indices/Exists/Types.php | 63 + .../Endpoints/Indices/Field/Get.php | 88 ++ .../Elasticsearch/Endpoints/Indices/Flush.php | 66 + .../Endpoints/Indices/ForceMerge.php | 57 + .../Endpoints/Indices/Gateway/Snapshot.php | 52 + .../Elasticsearch/Endpoints/Indices/Get.php | 79 + .../Endpoints/Indices/Mapping/Delete.php | 63 + .../Endpoints/Indices/Mapping/Get.php | 59 + .../Endpoints/Indices/Mapping/GetField.php | 79 + .../Endpoints/Indices/Mapping/Put.php | 96 ++ .../Elasticsearch/Endpoints/Indices/Open.php | 61 + .../Endpoints/Indices/Recovery.php | 52 + .../Endpoints/Indices/Refresh.php | 54 + .../Endpoints/Indices/Rollover.php | 109 ++ .../Elasticsearch/Endpoints/Indices/Seal.php | 50 + .../Endpoints/Indices/Segments.php | 54 + .../Endpoints/Indices/Settings/Get.php | 79 + .../Endpoints/Indices/Settings/Put.php | 86 ++ .../Endpoints/Indices/ShardStores.php | 59 + .../Endpoints/Indices/Shrink.php | 101 ++ .../Endpoints/Indices/Snapshotindex.php | 52 + .../Elasticsearch/Endpoints/Indices/Stats.php | 85 ++ .../Endpoints/Indices/Status.php | 56 + .../Template/AbstractTemplateEndpoint.php | 32 + .../Endpoints/Indices/Template/Delete.php | 78 + .../Endpoints/Indices/Template/Exists.php | 77 + .../Endpoints/Indices/Template/Get.php | 73 + .../Endpoints/Indices/Template/Put.php | 110 ++ .../Endpoints/Indices/Type/Exists.php | 60 + .../Endpoints/Indices/Upgrade/Get.php | 65 + .../Endpoints/Indices/Upgrade/Post.php | 65 + .../Endpoints/Indices/Validate/Query.php | 71 + .../Endpoints/Indices/ValidateQuery.php | 77 + .../lib/Elasticsearch/Endpoints/Info.php | 42 + .../Endpoints/Ingest/Pipeline/Delete.php | 54 + .../Endpoints/Ingest/Pipeline/Get.php | 51 + .../Endpoints/Ingest/Pipeline/Put.php | 71 + .../Endpoints/Ingest/Simulate.php | 65 + .../Elasticsearch/Endpoints/MPercolate.php | 78 + .../Elasticsearch/Endpoints/MTermVectors.php | 70 + .../lib/Elasticsearch/Endpoints/Mget.php | 93 ++ .../lib/Elasticsearch/Endpoints/Msearch.php | 103 ++ .../lib/Elasticsearch/Endpoints/Percolate.php | 98 ++ .../lib/Elasticsearch/Endpoints/Ping.php | 42 + .../lib/Elasticsearch/Endpoints/Reindex.php | 63 + .../Endpoints/RenderSearchTemplate.php | 77 + .../Elasticsearch/Endpoints/Script/Delete.php | 79 + .../Elasticsearch/Endpoints/Script/Get.php | 79 + .../Elasticsearch/Endpoints/Script/Put.php | 96 ++ .../lib/Elasticsearch/Endpoints/Scroll.php | 98 ++ .../lib/Elasticsearch/Endpoints/Search.php | 107 ++ .../Elasticsearch/Endpoints/SearchShards.php | 58 + .../Endpoints/SearchTemplate.php | 79 + .../Endpoints/Snapshot/Create.php | 119 ++ .../Endpoints/Snapshot/Delete.php | 101 ++ .../Elasticsearch/Endpoints/Snapshot/Get.php | 102 ++ .../Endpoints/Snapshot/Repository/Create.php | 107 ++ .../Endpoints/Snapshot/Repository/Delete.php | 77 + .../Endpoints/Snapshot/Repository/Get.php | 70 + .../Endpoints/Snapshot/Repository/Verify.php | 74 + .../Endpoints/Snapshot/Restore.php | 119 ++ .../Endpoints/Snapshot/Status.php | 100 ++ .../Elasticsearch/Endpoints/Source/Get.php | 78 + .../lib/Elasticsearch/Endpoints/Suggest.php | 85 ++ .../Elasticsearch/Endpoints/Tasks/Cancel.php | 71 + .../lib/Elasticsearch/Endpoints/Tasks/Get.php | 68 + .../Endpoints/Tasks/TasksList.php | 53 + .../Endpoints/Template/Delete.php | 51 + .../Elasticsearch/Endpoints/Template/Get.php | 51 + .../Elasticsearch/Endpoints/Template/Put.php | 68 + .../Elasticsearch/Endpoints/TermVectors.php | 91 ++ .../lib/Elasticsearch/Endpoints/Update.php | 99 ++ .../Helper/Iterators/SearchHitIterator.php | 161 ++ .../Iterators/SearchResponseIterator.php | 175 +++ .../Namespaces/AbstractNamespace.php | 77 + .../Namespaces/BooleanRequestWrapper.php | 58 + .../Elasticsearch/Namespaces/CatNamespace.php | 493 ++++++ .../Namespaces/ClusterNamespace.php | 205 +++ .../Namespaces/IndicesNamespace.php | 1163 ++++++++++++++ .../Namespaces/IngestNamespace.php | 114 ++ .../Namespaces/NamespaceBuilderInterface.php | 37 + .../Namespaces/NodesNamespace.php | 134 ++ .../Namespaces/SnapshotNamespace.php | 235 +++ .../Namespaces/TasksNamespace.php | 91 ++ .../Serializers/ArrayToJSONSerializer.php | 49 + .../EverythingToJSONSerializer.php | 45 + .../Serializers/SerializerInterface.php | 34 + .../Serializers/SmartSerializer.php | 88 ++ Framework/lib/Elasticsearch/Transport.php | 172 +++ Framework/lib/Log/AbstractLogger.php | 128 ++ .../lib/Log/InvalidArgumentException.php | 7 + Framework/lib/Log/LogLevel.php | 18 + Framework/lib/Log/LoggerAwareInterface.php | 18 + Framework/lib/Log/LoggerAwareTrait.php | 26 + Framework/lib/Log/LoggerInterface.php | 123 ++ Framework/lib/Log/LoggerTrait.php | 140 ++ Framework/lib/Log/NullLogger.php | 28 + .../Promise/CancellablePromiseInterface.php | 11 + Framework/lib/Promise/CancellationQueue.php | 55 + Framework/lib/Promise/Deferred.php | 42 + .../lib/Promise/Exception/LengthException.php | 7 + .../lib/Promise/ExtendedPromiseInterface.php | 21 + Framework/lib/Promise/FulfilledPromise.php | 69 + Framework/lib/Promise/LazyPromise.php | 54 + Framework/lib/Promise/Promise.php | 157 ++ Framework/lib/Promise/PromiseInterface.php | 11 + Framework/lib/Promise/PromisorInterface.php | 11 + .../lib/Promise/Queue/QueueInterface.php | 8 + .../lib/Promise/Queue/SynchronousQueue.php | 38 + Framework/lib/Promise/RejectedPromise.php | 77 + .../Promise/UnhandledRejectionException.php | 31 + Framework/lib/Promise/functions.php | 234 +++ Framework/lib/Promise/functions_include.php | 5 + Framework/lib/README.md | 20 + Framework/lib/Ring/Client/ClientUtils.php | 74 + Framework/lib/Ring/Client/CurlFactory.php | 560 +++++++ Framework/lib/Ring/Client/CurlHandler.php | 135 ++ .../lib/Ring/Client/CurlMultiHandler.php | 248 +++ Framework/lib/Ring/Client/Middleware.php | 58 + Framework/lib/Ring/Client/MockHandler.php | 52 + Framework/lib/Ring/Client/StreamHandler.php | 414 +++++ Framework/lib/Ring/Core.php | 364 +++++ .../lib/Ring/Exception/CancelledException.php | 7 + .../CancelledFutureAccessException.php | 4 + .../lib/Ring/Exception/ConnectException.php | 7 + .../lib/Ring/Exception/RingException.php | 4 + Framework/lib/Ring/Future/BaseFutureTrait.php | 125 ++ .../lib/Ring/Future/CompletedFutureArray.php | 43 + .../lib/Ring/Future/CompletedFutureValue.php | 57 + Framework/lib/Ring/Future/FutureArray.php | 40 + .../lib/Ring/Future/FutureArrayInterface.php | 11 + Framework/lib/Ring/Future/FutureInterface.php | 40 + Framework/lib/Ring/Future/FutureValue.php | 12 + .../lib/Ring/Future/MagicFutureTrait.php | 32 + Framework/lib/S3Helper | 1 + Framework/lib/Streams/AppendStream.php | 220 +++ Framework/lib/Streams/AsyncReadStream.php | 207 +++ Framework/lib/Streams/BufferStream.php | 138 ++ Framework/lib/Streams/CachingStream.php | 122 ++ Framework/lib/Streams/DroppingStream.php | 42 + .../Exception/CannotAttachException.php | 4 + .../lib/Streams/Exception/SeekException.php | 27 + Framework/lib/Streams/FnStream.php | 147 ++ Framework/lib/Streams/GuzzleStreamWrapper.php | 117 ++ Framework/lib/Streams/InflateStream.php | 27 + Framework/lib/Streams/LazyOpenStream.php | 37 + Framework/lib/Streams/LimitStream.php | 161 ++ .../lib/Streams/MetadataStreamInterface.php | 11 + Framework/lib/Streams/NoSeekStream.php | 25 + Framework/lib/Streams/NullStream.php | 79 + Framework/lib/Streams/PumpStream.php | 161 ++ Framework/lib/Streams/Stream.php | 261 ++++ .../lib/Streams/StreamDecoratorTrait.php | 144 ++ Framework/lib/Streams/StreamInterface.php | 159 ++ Framework/lib/Streams/Utils.php | 198 +++ Framework/phpunit.xml | 35 + Framework/src/Backends/AwsRestBackend.php | 47 + Framework/src/Backends/AwsS3Backend.php | 45 + Framework/src/Backends/DomBackend.php | 39 + Framework/src/Backends/ElasticBackend.php | 79 + Framework/src/Backends/FileBackend.php | 41 + Framework/src/Backends/InkBackend.php | 58 + .../src/Backends/MailBackendInterface.php | 13 + Framework/src/Backends/MailgunBackend.php | 74 + Framework/src/Backends/PostgresBackend.php | 109 ++ .../Streams/AbstractStreamWrapper.php | 74 + .../src/Bootstrap/AbstractBootstrapper.php | 138 ++ Framework/src/Configuration/Configuration.php | 84 + .../Configuration/ConfigurationInterface.php | 20 + .../src/Controllers/AbstractController.php | 105 ++ .../src/Controllers/ControllerInterface.php | 14 + .../src/Controllers/DeleteController.php | 11 + Framework/src/Controllers/GetController.php | 11 + Framework/src/Controllers/PatchController.php | 11 + Framework/src/Controllers/PostController.php | 11 + Framework/src/Controllers/PutController.php | 11 + .../Curl/Credentials/AbstractCredentials.php | 14 + Framework/src/Curl/Credentials/BasicAuth.php | 35 + .../src/Curl/Credentials/BearerToken.php | 29 + .../Curl/Credentials/CredentialsInterface.php | 13 + Framework/src/Curl/Curl.php | 50 + Framework/src/Curl/CurlHandler.php | 108 ++ Framework/src/Curl/RequestHeaders.php | 24 + Framework/src/Curl/RequestMethods/Delete.php | 19 + Framework/src/Curl/RequestMethods/Get.php | 19 + Framework/src/Curl/RequestMethods/Head.php | 19 + Framework/src/Curl/RequestMethods/Patch.php | 19 + Framework/src/Curl/RequestMethods/Post.php | 19 + .../RequestMethods/RequestMethodInterface.php | 13 + Framework/src/Curl/Response.php | 40 + .../src/DataStore/DataStoreInterface.php | 44 + Framework/src/DataStore/RedisBackend.php | 175 +++ Framework/src/Dom/Document.php | 100 ++ Framework/src/Dom/Element.php | 52 + Framework/src/Dom/Exception.php | 11 + Framework/src/Dom/Fragment.php | 11 + Framework/src/Dom/Node.php | 14 + .../ErrorHandlers/AbstractErrorHandler.php | 42 + .../src/Exceptions/FileUploadException.php | 44 + Framework/src/Exceptions/RouterException.php | 11 + .../src/Factories/AbstractChildFactory.php | 24 + Framework/src/Factories/BackendFactory.php | 98 ++ .../src/Factories/ChildFactoryInterface.php | 11 + Framework/src/Factories/FrameworkFactory.php | 78 + Framework/src/Factories/LoggerFactory.php | 46 + Framework/src/Factories/MasterFactory.php | 74 + .../src/Factories/MasterFactoryInterface.php | 20 + Framework/src/FrontController.php | 33 + .../src/Handlers/CommandHandlerInterface.php | 13 + .../src/Handlers/PostHandlerInterface.php | 13 + .../src/Handlers/PreHandlerInterface.php | 14 + .../src/Handlers/QueryHandlerInterface.php | 13 + .../src/Handlers/RequestHandlerInterface.php | 14 + .../src/Handlers/ResponseHandlerInterface.php | 14 + .../TransformationHandlerInterface.php | 13 + Framework/src/Http/Headers/Authorization.php | 55 + .../src/Http/Redirect/AbstractRedirect.php | 26 + .../src/Http/Redirect/PermanentRedirect.php | 17 + .../src/Http/Redirect/RedirectInterface.php | 16 + .../src/Http/Redirect/TemporaryRedirect.php | 17 + .../src/Http/Request/AbstractRequest.php | 111 ++ .../src/Http/Request/AbstractWriteRequest.php | 37 + Framework/src/Http/Request/DeleteRequest.php | 11 + Framework/src/Http/Request/GetRequest.php | 11 + Framework/src/Http/Request/PatchRequest.php | 11 + Framework/src/Http/Request/PostRequest.php | 40 + Framework/src/Http/Request/PutRequest.php | 11 + .../src/Http/Request/RequestInterface.php | 35 + .../Http/Request/WriteRequestInterface.php | 22 + .../src/Http/Response/AbstractResponse.php | 101 ++ Framework/src/Http/Response/HtmlResponse.php | 14 + Framework/src/Http/Response/JsonResponse.php | 14 + .../src/Http/Response/ResponseInterface.php | 26 + Framework/src/Http/StatusCodes/BadRequest.php | 17 + Framework/src/Http/StatusCodes/Created.php | 17 + Framework/src/Http/StatusCodes/Forbidden.php | 17 + .../Http/StatusCodes/InternalServerError.php | 17 + .../src/Http/StatusCodes/MethodNotAllowed.php | 17 + .../src/Http/StatusCodes/MovedPermanently.php | 17 + Framework/src/Http/StatusCodes/NotFound.php | 17 + Framework/src/Http/StatusCodes/SeeOther.php | 17 + .../Http/StatusCodes/StatusCodeInterface.php | 14 + .../src/Http/StatusCodes/Unauthorized.php | 17 + Framework/src/Languages/English.php | 14 + Framework/src/Languages/German.php | 14 + Framework/src/Languages/LanguageInterface.php | 11 + .../src/Logging/LoggerAwareInterface.php | 13 + Framework/src/Logging/LoggerAwareTrait.php | 26 + Framework/src/Logging/Loggers/Logger.php | 49 + .../src/Logging/Loggers/LoggerInterface.php | 15 + Framework/src/Logging/Loggers/NsaLogger.php | 41 + Framework/src/Logging/Loggers/SlackLogger.php | 50 + Framework/src/Logging/Logs/AbstractLog.php | 46 + Framework/src/Logging/Logs/EmergencyLog.php | 11 + Framework/src/Logging/Logs/ErrorLog.php | 11 + Framework/src/Mails/MailInterface.php | 17 + Framework/src/Map/Map.php | 48 + Framework/src/Map/MapInterface.php | 10 + Framework/src/Map/ReadableMapInterface.php | 13 + Framework/src/Map/WritableMapInterface.php | 13 + Framework/src/Models/AbstractModel.php | 11 + Framework/src/Pdo/Value/Boolean.php | 29 + Framework/src/Pdo/Value/NullValue.php | 19 + Framework/src/Pdo/Value/ValueInterface.php | 13 + Framework/src/Routers/Router.php | 45 + Framework/src/Routers/RouterInterface.php | 16 + Framework/src/Translation/Gettext.php | 48 + .../Translation/TranslatorAwareInterface.php | 11 + .../src/Translation/TranslatorAwareTrait.php | 24 + .../src/Translation/TranslatorInterface.php | 15 + Framework/src/ValueObjects/Cookie.php | 107 ++ Framework/src/ValueObjects/EmailAddress.php | 33 + Framework/src/ValueObjects/EmailPerson.php | 44 + Framework/src/ValueObjects/Header.php | 40 + Framework/src/ValueObjects/InkResult.php | 46 + Framework/src/ValueObjects/StringDateTime.php | 24 + Framework/src/ValueObjects/Timestamp.php | 29 + Framework/src/ValueObjects/Token.php | 28 + Framework/src/ValueObjects/UploadedFile.php | 71 + Framework/src/ValueObjects/Uri.php | 104 ++ Framework/tests/bootstrap.php | 10 + Framework/tests/data/config.ini | 9 + Framework/tests/data/invalid-config.ini | 1 + .../tests/unit/Backends/FileBackendTest.php | 51 + .../unit/Configuration/ConfigurationTest.php | 79 + .../Controllers/AbstractControllerTest.php | 116 ++ Framework/tests/unit/Curl/CurlTest.php | 135 ++ .../RequestMethods/RequestMethodsTest.php | 41 + Framework/tests/unit/Curl/ResponseTest.php | 27 + .../tests/unit/DataStore/RedisBackendTest.php | 122 ++ .../AbstractErrorHandlerTest.php | 29 + .../Exceptions/FileUploadExceptionTest.php | 38 + .../Http/Redirect/AbstractRedirectTest.php | 28 + .../Http/Redirect/PermanentRedirectTest.php | 27 + .../Http/Redirect/TemporaryRedirectTest.php | 27 + .../unit/Http/Request/PostRequestTest.php | 149 ++ .../unit/Http/StatusCodes/StatusCodesTest.php | 44 + .../tests/unit/Languages/LanguageTest.php | 30 + .../unit/Logging/LoggerAwareTraitTest.php | 34 + .../tests/unit/Logging/Loggers/LoggerTest.php | 55 + .../unit/Logging/Loggers/SlackLoggerTest.php | 100 ++ Framework/tests/unit/Map/MapTest.php | 70 + .../tests/unit/ValueObjects/CookieTest.php | 25 + .../unit/ValueObjects/EmailAddressTest.php | 28 + .../unit/ValueObjects/EmailPersonTest.php | 27 + .../tests/unit/ValueObjects/HeaderTest.php | 27 + .../tests/unit/ValueObjects/TokenTest.php | 27 + Framework/tests/unit/ValueObjects/UriTest.php | 65 + Frontend/Rakefile | 18 + Frontend/bootstrap.php | 4 + Frontend/config/system.ini | 1 + Frontend/data/templates/content/tracking.html | 9 + .../data/templates/content/verify/error.html | 17 + .../templates/content/verify/success.html | 23 + Frontend/data/templates/template.html | 69 + Frontend/index.php | 13 + Frontend/public/css | 1 + Frontend/public/favicon.ico | Bin 0 -> 90022 bytes Frontend/public/fonts | 1 + Frontend/public/icons | 1 + Frontend/public/images/logo.svg | 1 + Frontend/public/images/mono-logo.svg | 23 + Frontend/public/images/text-logo.svg | 9 + Frontend/public/images/triangle.svg | 8 + Frontend/public/js | 1 + Frontend/scripts/add-versions.sh | 22 + Frontend/src/Backends/ApiBackend.php | 61 + Frontend/src/Bootstrap/Bootstrapper.php | 110 ++ Frontend/src/Commands/AbstractApiCommand.php | 26 + .../src/Commands/CreateBetaRequestCommand.php | 14 + Frontend/src/Commands/CreateFeedCommand.php | 26 + Frontend/src/Commands/CreateNoteCommand.php | 26 + Frontend/src/Commands/CreateUploadCommand.php | 26 + .../src/Commands/DeleteFeedUserCommand.php | 26 + Frontend/src/Commands/DeletePostCommand.php | 26 + .../Feed/DeleteFeedInvitationCommand.php | 16 + .../src/Commands/Feed/FollowFeedCommand.php | 26 + .../src/Commands/Feed/UnfollowFeedCommand.php | 26 + .../Feed/UpdateFeedUserRoleCommand.php | 16 + .../src/Commands/InviteFeedUserCommand.php | 26 + Frontend/src/Commands/LoginCommand.php | 45 + Frontend/src/Commands/LogoutCommand.php | 38 + Frontend/src/Commands/RegisterCommand.php | 32 + .../Commands/ResendVerificationCommand.php | 26 + Frontend/src/Commands/VerifyCommand.php | 26 + Frontend/src/Commands/WriteSessionCommand.php | 28 + Frontend/src/DataObjects/ApiResponse.php | 41 + Frontend/src/DataObjects/User.php | 56 + Frontend/src/DataStore/DataStoreReader.php | 44 + Frontend/src/DataStore/DataStoreWriter.php | 32 + .../ErrorHandlers/DevelopmentErrorHandler.php | 51 + .../ErrorHandlers/ProductionErrorHandler.php | 40 + Frontend/src/Exceptions/AbstractException.php | 13 + Frontend/src/Exceptions/ApiException.php | 14 + Frontend/src/Exceptions/BadRequest.php | 16 + Frontend/src/Factories/ApplicationFactory.php | 45 + Frontend/src/Factories/CommandFactory.php | 135 ++ Frontend/src/Factories/ControllerFactory.php | 412 +++++ .../src/Factories/ErrorHandlerFactory.php | 21 + Frontend/src/Factories/FactoryTypeHint.php | 31 + .../src/Factories/FactoryTypeHintTrait.php | 16 + Frontend/src/Factories/HandlerFactory.php | 428 ++++++ Frontend/src/Factories/LocatorFactory.php | 23 + Frontend/src/Factories/QueryFactory.php | 90 ++ Frontend/src/Factories/RendererFactory.php | 342 +++++ Frontend/src/Factories/RouterFactory.php | 92 ++ Frontend/src/Factories/SessionFactory.php | 29 + .../src/Factories/TransformationFactory.php | 64 + Frontend/src/Gateways/ApiGateway.php | 289 ++++ Frontend/src/Handlers/CommandHandler.php | 17 + .../Get/Account/Verify/CommandHandler.php | 52 + .../Get/Account/Verify/RequestHandler.php | 26 + .../Get/CreatePostPage/QueryHandler.php | 24 + .../Handlers/Get/FeedPage/QueryHandler.php | 45 + .../Get/FeedPeoplePage/CommandHandler.php | 17 + .../Get/FeedPeoplePage/QueryHandler.php | 55 + .../Get/FeedPeoplePage/RequestHandler.php | 18 + .../Get/FeedPostsFragment/QueryHandler.php | 46 + .../Get/FeedPostsFragment/RequestHandler.php | 38 + .../Handlers/Get/FeedsPage/QueryHandler.php | 36 + .../Get/Fragment/TransformationHandler.php | 34 + .../Handlers/Get/Homepage/QueryHandler.php | 36 + .../HomepagePostsFragment/QueryHandler.php | 34 + .../HomepagePostsFragment/RequestHandler.php | 34 + Frontend/src/Handlers/Get/Page/PreHandler.php | 43 + .../Get/Page/TransformationHandler.php | 31 + .../Handlers/Get/PostPage/QueryHandler.php | 24 + .../Handlers/Get/SearchPage/QueryHandler.php | 55 + .../Get/SearchPage/RequestHandler.php | 23 + .../Handlers/Get/StaticPage/QueryHandler.php | 47 + .../Post/CreateBetaRequest/CommandHandler.php | 45 + .../Post/CreateBetaRequest/RequestHandler.php | 28 + .../Post/CreateNote/CommandHandler.php | 59 + .../Post/CreateNote/RequestHandler.php | 44 + .../DeleteFeedInvitation/CommandHandler.php | 38 + .../DeleteFeedInvitation/RequestHandler.php | 29 + .../Post/DeleteFeedUser/CommandHandler.php | 39 + .../Post/DeleteFeedUser/RequestHandler.php | 29 + .../Post/DeletePost/CommandHandler.php | 42 + .../Post/DeletePost/RequestHandler.php | 29 + .../Handlers/Post/Follow/CommandHandler.php | 40 + .../Handlers/Post/Follow/RequestHandler.php | 28 + .../Post/InviteFeedUser/CommandHandler.php | 48 + .../Post/InviteFeedUser/RequestHandler.php | 30 + .../Handlers/Post/Login/CommandHandler.php | 42 + .../Handlers/Post/Login/RequestHandler.php | 29 + .../Handlers/Post/Logout/CommandHandler.php | 37 + .../Handlers/Post/NewFeed/CommandHandler.php | 58 + .../Handlers/Post/NewFeed/RequestHandler.php | 32 + Frontend/src/Handlers/Post/PreHandler.php | 46 + .../Handlers/Post/Register/CommandHandler.php | 51 + .../Handlers/Post/Register/RequestHandler.php | 30 + .../ResendVerification/CommandHandler.php | 42 + .../ResendVerification/RequestHandler.php | 28 + .../Handlers/Post/TransformationHandler.php | 30 + .../Handlers/Post/Unfollow/CommandHandler.php | 40 + .../UpdateFeedUserRole/CommandHandler.php | 41 + .../UpdateFeedUserRole/RequestHandler.php | 30 + .../Handlers/Post/Upload/CommandHandler.php | 34 + .../Handlers/Post/Upload/RequestHandler.php | 29 + Frontend/src/Handlers/PostHandler.php | 35 + Frontend/src/Handlers/PreHandler.php | 18 + Frontend/src/Handlers/QueryHandler.php | 17 + Frontend/src/Handlers/RequestHandler.php | 18 + Frontend/src/Handlers/ResponseHandler.php | 42 + Frontend/src/Locators/SearchTabLocator.php | 28 + Frontend/src/Locators/StatusCodeLocator.php | 23 + Frontend/src/Models/Account/NewFeedModel.php | 56 + Frontend/src/Models/Account/VerifyModel.php | 26 + .../Models/Action/CreateBetaRequestModel.php | 26 + .../src/Models/Action/CreateNoteModel.php | 71 + .../src/Models/Action/DeleteFeedUserModel.php | 41 + .../src/Models/Action/DeletePostModel.php | 41 + Frontend/src/Models/Action/FollowModel.php | 26 + .../src/Models/Action/InviteFeedUserModel.php | 56 + Frontend/src/Models/Action/LoginModel.php | 41 + Frontend/src/Models/Action/RegisterModel.php | 56 + .../Models/Action/ResendVerificationModel.php | 26 + .../Models/Action/UpdateFeedUserRoleModel.php | 56 + Frontend/src/Models/Action/UploadModel.php | 41 + Frontend/src/Models/ActionModel.php | 24 + Frontend/src/Models/CreatePostPageModel.php | 24 + Frontend/src/Models/FeedsPageModel.php | 26 + .../Fragment/FeedPostsFragmentModel.php | 87 ++ .../Fragment/HomepagePostsFragmentModel.php | 57 + Frontend/src/Models/FragmentModel.php | 11 + Frontend/src/Models/FrontendModel.php | 88 ++ Frontend/src/Models/HomepageModel.php | 26 + Frontend/src/Models/Page/FeedPageModel.php | 40 + .../src/Models/Page/FeedPeoplePageModel.php | 47 + .../src/Models/Page/FeedPostsPageModel.php | 26 + Frontend/src/Models/Page/SearchPageModel.php | 74 + Frontend/src/Models/PageModel.php | 59 + Frontend/src/Models/PostPageModel.php | 24 + Frontend/src/Models/StaticPageModel.php | 53 + .../Feed/FetchFeedInvitationsQuery.php | 26 + .../src/Queries/Feed/FetchFeedUsersQuery.php | 26 + .../src/Queries/Feed/LookupVanityQuery.php | 30 + Frontend/src/Queries/FetchFeedPostsQuery.php | 34 + Frontend/src/Queries/FetchFeedQuery.php | 26 + Frontend/src/Queries/FetchStaticPageQuery.php | 28 + Frontend/src/Queries/FetchUserFeedQuery.php | 30 + Frontend/src/Queries/FetchUserFeedsQuery.php | 30 + Frontend/src/Queries/IsLoggedInQuery.php | 26 + Frontend/src/Queries/Post/FetchPostQuery.php | 26 + Frontend/src/Queries/SearchQuery.php | 29 + Frontend/src/Renderers/FeedPageRenderer.php | 82 + .../Fragment/FeedPostsFragmentRenderer.php | 44 + .../Renderers/Fragment/FragmentRenderer.php | 13 + .../HomepagePostsFragmentRenderer.php | 42 + .../Account/VerifyAccountPageRenderer.php | 45 + .../Renderers/Page/CreatePostPageRenderer.php | 178 +++ .../Page/Feed/FeedPeoplePageRenderer.php | 142 ++ .../src/Renderers/Page/FeedPageRenderer.php | 66 + .../src/Renderers/Page/FeedsPageRenderer.php | 106 ++ .../src/Renderers/Page/HomepageRenderer.php | 95 ++ .../Renderers/Page/PageRendererInterface.php | 14 + .../src/Renderers/Page/PostPageRenderer.php | 45 + .../src/Renderers/Page/SearchPageRenderer.php | 96 ++ .../src/Renderers/Page/StaticPageRenderer.php | 44 + Frontend/src/Renderers/PageRenderer.php | 56 + Frontend/src/Renderers/Renderer.php | 13 + .../Renderers/Snippet/AjaxButtonSnippet.php | 21 + .../Renderers/Snippet/FeedButtonsSnippet.php | 64 + .../src/Renderers/Snippet/FeedCardSnippet.php | 61 + .../Renderers/Snippet/FeedHeaderSnippet.php | 80 + .../Snippet/FeedInvitationBannerSnippet.php | 25 + .../Snippet/FeedInvitationCardSnippet.php | 68 + .../src/Renderers/Snippet/FeedListSnippet.php | 61 + .../Snippet/FeedNavigationSnippet.php | 56 + .../Renderers/Snippet/FeedUserCardSnippet.php | 107 ++ .../Snippet/FloatingButtonSnippet.php | 42 + .../Snippet/HomepageNavigationSnippet.php | 38 + .../Snippet/HomepageOnboardingSnippet.php | 36 + .../Renderers/Snippet/IconButtonSnippet.php | 48 + .../src/Renderers/Snippet/IconSnippet.php | 23 + .../Snippet/PaginationButtonSnippet.php | 31 + .../Snippet/PostAttachmentSnippet.php | 50 + .../src/Renderers/Snippet/PostSnippet.php | 159 ++ .../Renderers/Snippet/SearchTabNavSnippet.php | 44 + .../src/Renderers/Snippet/TabNavSnippet.php | 51 + .../Snippet/UserRolesOptionsSnippet.php | 42 + Frontend/src/Routers/ActionRouter.php | 46 + Frontend/src/Routers/FeedPageRouter.php | 101 ++ Frontend/src/Routers/FragmentRouter.php | 46 + Frontend/src/Routers/NotFoundRouter.php | 34 + Frontend/src/Routers/PageRouter.php | 40 + Frontend/src/Routers/PostPageRouter.php | 55 + Frontend/src/Routers/StaticPageRouter.php | 50 + Frontend/src/Routers/UserActionRouter.php | 71 + Frontend/src/Routers/UserFragmentRouter.php | 51 + Frontend/src/Routers/UserPageRouter.php | 59 + Frontend/src/Session/Session.php | 177 +++ .../src/TabNavItems/AbstractTabNavItem.php | 24 + Frontend/src/TabNavItems/FeedPage/People.php | 27 + Frontend/src/TabNavItems/FeedPage/Posts.php | 27 + .../src/TabNavItems/FeedPage/Settings.php | 27 + Frontend/src/TabNavItems/Homepage/Feeds.php | 32 + Frontend/src/TabNavItems/Homepage/Posts.php | 32 + .../src/TabNavItems/SearchPage/Everything.php | 27 + Frontend/src/TabNavItems/SearchPage/Feeds.php | 27 + Frontend/src/TabNavItems/SearchPage/Posts.php | 27 + Frontend/src/TabNavItems/TabNavItem.php | 19 + Frontend/src/Tabs/FeedPage/People.php | 13 + Frontend/src/Tabs/FeedPage/Posts.php | 13 + Frontend/src/Tabs/FeedPage/Settings.php | 13 + Frontend/src/Tabs/Homepage/Feeds.php | 13 + Frontend/src/Tabs/Homepage/Posts.php | 13 + Frontend/src/Tabs/SearchPage/Everything.php | 13 + Frontend/src/Tabs/SearchPage/Feeds.php | 13 + Frontend/src/Tabs/SearchPage/Posts.php | 13 + Frontend/src/Tabs/Tab.php | 11 + .../CanonicalUriTransformation.php | 26 + .../CsrfTokenTransformation.php | 19 + .../Transformations/TitleTransformation.php | 20 + .../TrackingTransformation.php | 36 + Frontend/src/Transformations/Transformer.php | 30 + .../UserDropdownTransformation.php | 36 + Frontend/src/ValueObjects/Feed.php | 83 + Frontend/src/ValueObjects/PaginatedResult.php | 87 ++ Ink | 1 + LICENSE | 662 ++++++++ Library/Rakefile | 18 + Library/bootstrap.php | 2 + .../Backends/Streams/DataStreamWrapper.php | 31 + .../Backends/Streams/PagesStreamWrapper.php | 31 + .../Streams/TemplatesStreamWrapper.php | 31 + Library/src/Builders/UriBuilder.php | 90 ++ Library/src/DataObjects/FeedInvitation.php | 48 + Library/src/DataObjects/StaticPage.php | 64 + .../src/DataStore/AbstractDataStoreReader.php | 106 ++ .../src/DataStore/AbstractDataStoreWriter.php | 83 + Library/src/Factories/ApplicationFactory.php | 19 + Library/src/Factories/IndexerFactory.php | 32 + Library/src/Factories/LocatorFactory.php | 21 + Library/src/Factories/MapperFactory.php | 30 + Library/src/Factories/ServiceFactory.php | 18 + Library/src/Indexers/FeedIndexer.php | 33 + Library/src/Indexers/Indexer.php | 13 + Library/src/Indexers/PostIndexer.php | 34 + Library/src/Indexers/UserIndexer.php | 31 + Library/src/Locators/SearchTypeLocator.php | 23 + Library/src/Locators/UserRoleLocator.php | 23 + Library/src/Mappers/DocumentMapper.php | 41 + Library/src/Mappers/FeedMapper.php | 51 + Library/src/Mappers/PostMapper.php | 65 + Library/src/PostTypes/Event.php | 14 + Library/src/PostTypes/Note.php | 14 + Library/src/PostTypes/PostTypeInterface.php | 11 + Library/src/PostTypes/Task.php | 14 + Library/src/SearchTypes/All.php | 19 + Library/src/SearchTypes/Feed.php | 19 + Library/src/SearchTypes/Post.php | 19 + Library/src/SearchTypes/SearchType.php | 13 + .../src/Services/FeedInvitationService.php | 103 ++ Library/src/TaskPriorities/High.php | 14 + Library/src/TaskPriorities/Low.php | 14 + Library/src/TaskPriorities/Normal.php | 14 + Library/src/TaskPriorities/Priority.php | 11 + Library/src/TaskPriorities/Random.php | 24 + Library/src/Tasks/BuildFeedTask.php | 29 + Library/src/Tasks/BuildFeedsTask.php | 14 + Library/src/Tasks/BuildPostTask.php | 41 + Library/src/Tasks/BuildPostsTask.php | 14 + Library/src/Tasks/BuildStaticPagesTask.php | 14 + Library/src/Tasks/DeleteUnusedFilesTask.php | 14 + Library/src/Tasks/IndexFeedTask.php | 29 + Library/src/Tasks/IndexFeedsTask.php | 14 + Library/src/Tasks/IndexPostTask.php | 29 + Library/src/Tasks/IndexPostsTask.php | 14 + Library/src/Tasks/IndexUserTask.php | 32 + Library/src/Tasks/IndexUsersTask.php | 14 + Library/src/Tasks/InitialTask.php | 14 + Library/src/Tasks/SendFeedInvitationTask.php | 31 + .../src/Tasks/SendVerificationEmailTask.php | 43 + Library/src/Tasks/TaskInterface.php | 11 + .../TransformationInterface.php | 14 + .../TranslateTransformation.php | 42 + Library/src/UserRoles/DefaultUserRole.php | 19 + Library/src/UserRoles/Moderator.php | 19 + Library/src/UserRoles/Owner.php | 19 + Library/src/UserRoles/UserRole.php | 13 + Library/src/ValueObjects/DisplayName.php | 33 + Locale/Rakefile | 30 + Locale/de_CH/LC_MESSAGES/messages.po | 72 + Locale/en_GB/LC_MESSAGES/messages.po | 48 + README.md | 3 + Rakefile | 25 + Showcase/css | 1 + Showcase/favicon.ico | 1 + Showcase/feed.html | 162 ++ Showcase/icons | 1 + Showcase/images | 1 + Showcase/index.html | 112 ++ Showcase/js | 1 + Showcase/new-post.html | 112 ++ Styles/README.md | 3 + Styles/Rakefile | 24 + Styles/browserslist | 7 + Styles/fonts/README.md | 1 + Styles/fonts/vollkorn-medium-webfont.woff | Bin 0 -> 58096 bytes Styles/icons/README.md | 2 + Styles/icons/actions/delete.svg | 12 + Styles/icons/actions/invite.svg | 10 + Styles/icons/actions/post.svg | 13 + Styles/icons/attachment.svg | 10 + Styles/icons/done.svg | 10 + Styles/icons/event.svg | 10 + Styles/icons/feed.svg | 10 + Styles/icons/label.svg | 10 + Styles/icons/note.svg | 12 + Styles/icons/options.svg | 10 + Styles/icons/person.svg | 10 + Styles/icons/private.svg | 10 + Styles/icons/public.svg | 10 + Styles/icons/search.svg | 10 + Styles/icons/settings.svg | 10 + Styles/icons/task.svg | 10 + Styles/icons/twitter.svg | 10 + Styles/icons/verified.svg | 11 + Styles/less/_helpers.less | 36 + Styles/less/_mixins.less | 7 + Styles/less/_variables.less | 54 + Styles/less/application.less | 16 + Styles/less/components/all.less | 18 + Styles/less/components/basic/all.less | 8 + .../less/components/basic/basic-button.less | 56 + .../components/basic/basic-heading-a.less | 11 + .../components/basic/basic-heading-b.less | 6 + Styles/less/components/basic/basic-icon.less | 16 + Styles/less/components/basic/basic-input.less | 16 + Styles/less/components/basic/basic-link.less | 16 + .../components/basic/basic-paragraph.less | 6 + Styles/less/components/basic/basic-pre.less | 6 + Styles/less/components/feed-list.less | 28 + Styles/less/components/feed/all.less | 2 + Styles/less/components/feed/feed-card.less | 28 + Styles/less/components/feed/feed-header.less | 67 + Styles/less/components/flex-container.less | 14 + Styles/less/components/floating/all.less | 2 + .../components/floating/floating-button.less | 61 + .../components/floating/floating-buttons.less | 27 + Styles/less/components/form/all.less | 4 + Styles/less/components/form/form-box.less | 12 + .../less/components/form/form-checkbox.less | 11 + Styles/less/components/form/form-error.less | 9 + Styles/less/components/form/form-field.less | 26 + Styles/less/components/generic-card.less | 14 + Styles/less/components/light/all.less | 3 + .../less/components/light/light-button.less | 85 ++ Styles/less/components/light/light-pill.less | 4 + .../less/components/light/light-select.less | 19 + Styles/less/components/logo-link.less | 6 + Styles/less/components/page/all.less | 4 + Styles/less/components/page/page-banner.less | 11 + Styles/less/components/page/page-footer.less | 64 + Styles/less/components/page/page-header.less | 72 + Styles/less/components/page/page-wrapper.less | 25 + Styles/less/components/pagination-button.less | 25 + Styles/less/components/post/all.less | 5 + .../less/components/post/post-attachment.less | 84 + .../post/post-card-outside-text.less | 10 + Styles/less/components/post/post-card.less | 92 ++ Styles/less/components/post/post-content.less | 58 + Styles/less/components/post/post-list.less | 31 + Styles/less/components/search/all.less | 1 + .../less/components/search/search-form.less | 42 + Styles/less/components/survey/all.less | 1 + .../survey/survey-question-card.less | 33 + Styles/less/components/tab-nav.less | 54 + Styles/less/components/task/all.less | 1 + .../less/components/task/task-checkbox.less | 33 + Styles/less/components/toast/all.less | 2 + .../components/toast/toast-container.less | 6 + .../less/components/toast/toast-message.less | 39 + Styles/less/components/user/all.less | 2 + .../less/components/user/user-list-item.less | 33 + Styles/less/components/user/user-list.less | 16 + Styles/less/core/all.less | 4 + Styles/less/core/basics.less | 19 + Styles/less/core/normalize.less | 68 + Styles/less/core/reset.less | 49 + Styles/less/core/typography.less | 7 + Styles/less/elements/all.less | 1 + Styles/less/elements/form-error.less | 11 + Styles/less/fonts/vollkorn.less | 8 + Survey/Rakefile | 18 + Survey/bootstrap.php | 5 + Survey/config/system.ini | 1 + Survey/data/templates/template.html | 29 + Survey/index.php | 13 + Survey/src/Bootstrap/Bootstrapper.php | 99 ++ Survey/src/Builders/UriBuilder.php | 24 + .../Commands/ApproveBetaRequestCommand.php | 33 + Survey/src/Commands/InsertAnswerCommand.php | 35 + Survey/src/DataObjects/Question.php | 29 + Survey/src/Factories/ApplicationFactory.php | 18 + Survey/src/Factories/CommandFactory.php | 25 + Survey/src/Factories/ControllerFactory.php | 43 + Survey/src/Factories/HandlerFactory.php | 47 + Survey/src/Factories/QueryFactory.php | 25 + Survey/src/Factories/RendererFactory.php | 32 + Survey/src/Factories/RouterFactory.php | 26 + .../Handlers/Get/SurveyPage/QueryHandler.php | 32 + .../Handlers/Post/Survey/CommandHandler.php | 44 + .../src/Handlers/Post/Survey/QueryHandler.php | 75 + .../Handlers/Post/Survey/RequestHandler.php | 29 + .../src/Models/Action/SurveyActionModel.php | 71 + Survey/src/Models/Page/SurveyPageModel.php | 41 + Survey/src/Queries/FetchBetaRequestQuery.php | 31 + Survey/src/Queries/FetchQuestionsQuery.php | 28 + .../PageContent/SurveyPageContentRenderer.php | 104 ++ Survey/src/Routers/ActionRouter.php | 40 + Survey/src/Routers/BetaSurveyRouter.php | 54 + Survey/src/ValueObjects/AnswerValue.php | 28 + Worker/Rakefile | 18 + Worker/bootstrap.php | 4 + Worker/config/system.ini | 1 + Worker/data/mails/invitation.xhtml | 72 + Worker/data/mails/verification.xhtml | 60 + Worker/data/pages/404.json | 6 + Worker/data/pages/beta-thanks.json | 5 + Worker/data/pages/beta.json | 5 + Worker/data/pages/create-feed.json | 4 + Worker/data/pages/error.json | 6 + Worker/data/pages/homepage.json | 4 + Worker/data/pages/login.json | 5 + Worker/data/pages/register-confirmation.json | 5 + Worker/data/pages/register.json | 5 + Worker/data/pages/resend-verification.json | 5 + Worker/data/pages/survey-thanks.json | 5 + Worker/data/routes.json | 13 + Worker/data/templates/content/404.html | 12 + .../content/account/create-feed.html | 39 + .../data/templates/content/beta/request.html | 29 + .../data/templates/content/beta/thanks.html | 22 + Worker/data/templates/content/error.html | 13 + Worker/data/templates/content/homepage.html | 3 + Worker/data/templates/content/login.html | 43 + .../content/register-confirmation.html | 23 + Worker/data/templates/content/register.html | 51 + .../content/resend-verification.html | 27 + .../data/templates/content/survey/thanks.html | 29 + Worker/push.php | 26 + Worker/src/Bootstrapper.php | 70 + Worker/src/Builders/StaticPageBuilder.php | 70 + Worker/src/DataStore/DataStoreReader.php | 16 + Worker/src/DataStore/DataStoreWriter.php | 54 + Worker/src/Factories/ApplicationFactory.php | 71 + Worker/src/Factories/LocatorFactory.php | 30 + Worker/src/Factories/MailFactory.php | 27 + Worker/src/Factories/RendererFactory.php | 26 + Worker/src/Factories/RunnerFactory.php | 140 ++ Worker/src/Locators/RunnerLocator.php | 64 + Worker/src/Locators/StatusCodeLocator.php | 23 + Worker/src/Locators/TaskLocator.php | 35 + Worker/src/Mails/AbstractMail.php | 43 + Worker/src/Mails/FeedInvitationMail.php | 100 ++ Worker/src/Mails/VerificationMail.php | 56 + Worker/src/Renderers/StaticPageRenderer.php | 37 + Worker/src/Runners/BuildFeedRunner.php | 69 + Worker/src/Runners/BuildFeedsRunner.php | 41 + Worker/src/Runners/BuildPostRunner.php | 56 + Worker/src/Runners/BuildPostsRunner.php | 47 + Worker/src/Runners/BuildStaticPagesRunner.php | 61 + .../src/Runners/DeleteUnusedFilesRunner.php | 55 + Worker/src/Runners/IndexFeedRunner.php | 66 + Worker/src/Runners/IndexFeedsRunner.php | 41 + Worker/src/Runners/IndexPostRunner.php | 68 + Worker/src/Runners/IndexPostsRunner.php | 43 + Worker/src/Runners/IndexUserRunner.php | 44 + Worker/src/Runners/IndexUsersRunner.php | 41 + Worker/src/Runners/InitialRunner.php | 33 + Worker/src/Runners/RunnerInterface.php | 13 + .../src/Runners/SendFeedInvitationRunner.php | 68 + .../Runners/SendVerificationEmailRunner.php | 42 + Worker/src/Services/FeedService.php | 69 + Worker/src/Services/FileService.php | 42 + Worker/src/Services/PostService.php | 40 + Worker/src/Services/UserService.php | 49 + .../TransformationInterface.php | 13 + .../TranslateTransformation.php | 41 + Worker/src/Worker.php | 61 + Worker/worker.php | 13 + containers/ttio-api/Dockerfile | 22 + containers/ttio-dev-frontend/Dockerfile | 4 + containers/ttio-dev-frontend/ttio-root-ca.crt | 34 + containers/ttio-dev-proxy/Dockerfile | 9 + containers/ttio-dev-proxy/certs/fullchain.pem | 98 ++ containers/ttio-dev-proxy/certs/privkey.pem | 27 + .../nginx/conf.d/beta.timetab.io.conf | 17 + .../nginx/conf.d/coverage.timetab.io.conf | 22 + .../nginx/conf.d/dev.timetab.io.conf | 31 + .../nginx/conf.d/devapi.timetab.io.conf | 13 + .../nginx/conf.d/showcase.timetab.io.conf | 31 + .../nginx/conf.d/survey.timetab.io.conf | 30 + containers/ttio-dev-proxy/nginx/nginx.conf | 38 + containers/ttio-dev-survey/Dockerfile | 4 + containers/ttio-dev-survey/ttio-root-ca.crt | 34 + containers/ttio-frontend/Dockerfile | 34 + .../ttio-postgres/patches/003-survey.sql | 23 + containers/ttio-proxy/Dockerfile | 19 + .../config/nginx/conf.d/api.timetab.io.conf | 14 + .../config/nginx/conf.d/beta.timetab.io.conf | 22 + .../config/nginx/conf.d/default.conf | 13 + .../nginx/conf.d/docker.ttio.cloud.conf | 11 + .../nginx/conf.d/survey.timetab.io.conf | 39 + .../config/nginx/conf.d/timetab.io.conf | 48 + containers/ttio-proxy/config/nginx/nginx.conf | 37 + containers/ttio-proxy/config/nginx/ssl_config | 17 + containers/ttio-staging/Dockerfile | 15 + containers/ttio-staging/config/docker.repo | 6 + containers/ttio-survey/Dockerfile | 45 + containers/ttio-worker/Dockerfile | 29 + data/elastic-mappings.json | 127 ++ data/patches/001-feed-vanity-update.sql | 1 + data/patches/002-feed-description.sql | 20 + data/schema.sql | 224 +++ packages/ttio-docker-registry/auth/htpasswd | 3 + packages/ttio-docker-registry/package.spec | 42 + .../units/ttio-docker-registry.service | 25 + .../config/letsencrypt/docker.ttio.cloud.ini | 7 + .../config/letsencrypt/timetab.io.ini | 7 + packages/ttio-server/config/renew-certs.sh | 14 + packages/ttio-server/cron.d/renew-certs | 1 + packages/ttio-server/package.spec | 41 + packages/ttio-web/cron.d/run-tasks | 1 + packages/ttio-web/package.spec | 99 ++ packages/ttio-web/units/ttio-api.service | 18 + packages/ttio-web/units/ttio-elastic.service | 16 + packages/ttio-web/units/ttio-frontend.service | 18 + packages/ttio-web/units/ttio-postgres.service | 16 + packages/ttio-web/units/ttio-proxy.service | 21 + packages/ttio-web/units/ttio-redis.service | 16 + packages/ttio-web/units/ttio-survey.service | 18 + packages/ttio-web/units/ttio-web.target | 12 + packages/ttio-web/units/ttio-worker@.service | 18 + packages/ttio-web/units/ttio-workers.target | 4 + rake/gen_autoload.rb | 12 + scripts/attach-workers.sh | 14 + scripts/build-containers.sh | 36 + scripts/build-dev-containers.sh | 9 + scripts/build-packages.sh | 33 + scripts/create-icon.php | 42 + scripts/deploy.sh | 21 + scripts/pull-containers.sh | 13 + scripts/push-containers.sh | 19 + scripts/push-task.sh | 3 + scripts/rake.sh | 3 + scripts/release-packages.sh | 65 + scripts/resty-auth.sh | 18 + scripts/setup.sh | 3 + scripts/spawn-workers.sh | 21 + scripts/staging-release.sh | 77 + scripts/start-the-magic.sh | 202 +++ 1329 files changed, 66741 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .travis.yml create mode 100644 API/Rakefile create mode 100644 API/bootstrap.php create mode 120000 API/config/system.ini create mode 100644 API/index.php create mode 100644 API/phpunit.xml create mode 100755 API/scripts/create-feeds.sh create mode 100755 API/scripts/create-system-token.php create mode 100755 API/scripts/create-user.sh create mode 100644 API/src/Access/AccessControl.php create mode 100644 API/src/Access/AccessControl/AbstractAccessControl.php create mode 100644 API/src/Access/AccessControl/CollectionAccessControl.php create mode 100644 API/src/Access/AccessControl/FeedAccessControl.php create mode 100644 API/src/Access/AccessTypes/AccessTypeInterface.php create mode 100644 API/src/Access/AccessTypes/FullAccess.php create mode 100644 API/src/Access/AccessTypes/NoAccess.php create mode 100644 API/src/Access/AccessTypes/ScopedAccess.php create mode 100644 API/src/Access/AccessTypes/SystemAccess.php create mode 100644 API/src/Backends/SearchBackend.php create mode 100644 API/src/Bootstrap/Bootstrapper.php create mode 100644 API/src/Builders/UriBuilder.php create mode 100644 API/src/Commands/BetaRequest/CreateBetaRequestCommand.php create mode 100644 API/src/Commands/CreateCollectionCommand.php create mode 100644 API/src/Commands/CreateFeedCommand.php create mode 100644 API/src/Commands/DeleteAccessTokenCommand.php create mode 100644 API/src/Commands/DeleteCollectionCommand.php create mode 100644 API/src/Commands/Feed/CreateFeedUploadUrlCommand.php create mode 100644 API/src/Commands/Feed/CreateInvitationCommand.php create mode 100644 API/src/Commands/Feed/DeleteInvitationCommand.php create mode 100644 API/src/Commands/Feed/SetFeedVanityCommand.php create mode 100644 API/src/Commands/Feed/UpdateFeedUserCommand.php create mode 100644 API/src/Commands/Feed/UpdateInvitationCommand.php create mode 100644 API/src/Commands/Feeds/CreateFeedPersonCommand.php create mode 100644 API/src/Commands/Feeds/DeleteFeedPersonCommand.php create mode 100644 API/src/Commands/Feeds/FollowFeedCommand.php create mode 100644 API/src/Commands/Feeds/UnfollowFeedCommand.php create mode 100644 API/src/Commands/Feeds/UpdateFeedCommand.php create mode 100644 API/src/Commands/File/CreateFileCommand.php create mode 100644 API/src/Commands/Posts/CreatePostCommand.php create mode 100644 API/src/Commands/Posts/DeletePostCommand.php create mode 100644 API/src/Commands/SaveAccessTokenCommand.php create mode 100644 API/src/Commands/SendVerificationCommand.php create mode 100644 API/src/Commands/UpdateCollectionCommand.php create mode 100644 API/src/Commands/User/CreateUserCommand.php create mode 100644 API/src/Commands/User/UpdateUserCommand.php create mode 100644 API/src/Commands/User/VerifyUserCommand.php create mode 100644 API/src/DataStore/DataStoreReader.php create mode 100644 API/src/DataStore/DataStoreWriter.php create mode 100644 API/src/Endpoints/AbstractEndpoint.php create mode 100644 API/src/Endpoints/AuthEndpoint.php create mode 100644 API/src/Endpoints/BetaRequest/CreateBetaRequestEndpoint.php create mode 100644 API/src/Endpoints/Collections/CreateCollectionEndpoint.php create mode 100644 API/src/Endpoints/Collections/DeleteCollectionEndpoint.php create mode 100644 API/src/Endpoints/Collections/GetCollectionEndpoint.php create mode 100644 API/src/Endpoints/Collections/UpdateCollectionEndpoint.php create mode 100644 API/src/Endpoints/EndpointInterface.php create mode 100644 API/src/Endpoints/Feeds/CreateFeedEndpoint.php create mode 100644 API/src/Endpoints/Feeds/CreateInvitationEndpoint.php create mode 100644 API/src/Endpoints/Feeds/CreateUploadEndpoint.php create mode 100644 API/src/Endpoints/Feeds/DeleteFeedInvitationEndpoint.php create mode 100644 API/src/Endpoints/Feeds/DeleteFeedUserEndpoint.php create mode 100644 API/src/Endpoints/Feeds/FollowFeedEndpoint.php create mode 100644 API/src/Endpoints/Feeds/GetFeedEndpoint.php create mode 100644 API/src/Endpoints/Feeds/GetFeedInvitationsEndpoint.php create mode 100644 API/src/Endpoints/Feeds/GetFeedUsersEndpoint.php create mode 100644 API/src/Endpoints/Feeds/GetFeedsEndpoint.php create mode 100644 API/src/Endpoints/Feeds/UnfollowFeedEndpoint.php create mode 100644 API/src/Endpoints/Feeds/UpdateFeedEndpoint.php create mode 100644 API/src/Endpoints/Feeds/UpdateFeedInvitationEndpoint.php create mode 100644 API/src/Endpoints/Feeds/UpdateFeedUserEndpoint.php create mode 100644 API/src/Endpoints/Index/GetIndexEndpoint.php create mode 100644 API/src/Endpoints/Posts/CreatePostEndpoint.php create mode 100644 API/src/Endpoints/Posts/DeletePostEndpoint.php create mode 100644 API/src/Endpoints/Posts/GetPostEndpoint.php create mode 100644 API/src/Endpoints/Posts/GetPostsEndpoint.php create mode 100644 API/src/Endpoints/Posts/UpdatePostEndpoint.php create mode 100644 API/src/Endpoints/Profiles/GetProfileEndpoint.php create mode 100644 API/src/Endpoints/Random/GetRandomEndpoint.php create mode 100644 API/src/Endpoints/RevokeEndpoint.php create mode 100644 API/src/Endpoints/SearchEndpoint.php create mode 100644 API/src/Endpoints/User/GetTodoEndpoint.php create mode 100644 API/src/Endpoints/User/GetUpcomingEndpoint.php create mode 100644 API/src/Endpoints/User/GetUserCollectionsEndpoint.php create mode 100644 API/src/Endpoints/User/GetUserEndpoint.php create mode 100644 API/src/Endpoints/User/GetUserFeedEndpoint.php create mode 100644 API/src/Endpoints/User/GetUserFeedsEndpoint.php create mode 100644 API/src/Endpoints/User/UpdateUserEndpoint.php create mode 100644 API/src/Endpoints/User/UpdateUserPasswordEndpoint.php create mode 100644 API/src/Endpoints/Users/CreateUserEndpoint.php create mode 100644 API/src/Endpoints/Verify/ResendEndpoint.php create mode 100644 API/src/Endpoints/Verify/VerifyEndpoint.php create mode 100644 API/src/ErrorHandlers/DevelopmentErrorHandler.php create mode 100644 API/src/ErrorHandlers/ProductionErrorHandler.php create mode 100644 API/src/Exceptions/AbstractException.php create mode 100644 API/src/Exceptions/BadRequest.php create mode 100644 API/src/Exceptions/Forbidden.php create mode 100644 API/src/Exceptions/MethodNotAllowed.php create mode 100644 API/src/Exceptions/NotFound.php create mode 100644 API/src/Exceptions/Unauthorized.php create mode 100644 API/src/Factories/ApplicationFactory.php create mode 100644 API/src/Factories/BackendFactory.php create mode 100644 API/src/Factories/CommandFactory.php create mode 100644 API/src/Factories/ControllerFactory.php create mode 100644 API/src/Factories/EndpointFactory.php create mode 100644 API/src/Factories/ErrorHandlerFactory.php create mode 100644 API/src/Factories/HandlerFactory.php create mode 100644 API/src/Factories/MapperFactory.php create mode 100644 API/src/Factories/QueryFactory.php create mode 100644 API/src/Factories/RouterFactory.php create mode 100644 API/src/Factories/ServiceFactory.php create mode 100644 API/src/Handlers/CommandHandler.php create mode 100644 API/src/Handlers/Delete/Collection/CommandHandler.php create mode 100644 API/src/Handlers/Delete/Collection/QueryHandler.php create mode 100644 API/src/Handlers/Delete/Collection/RequestHandler.php create mode 100644 API/src/Handlers/Delete/Feed/Invitation/CommandHandler.php create mode 100644 API/src/Handlers/Delete/Feed/Invitation/QueryHandler.php create mode 100644 API/src/Handlers/Delete/Feed/Invitation/RequestHandler.php create mode 100644 API/src/Handlers/Delete/Feed/People/CommandHandler.php create mode 100644 API/src/Handlers/Delete/Feed/People/QueryHandler.php create mode 100644 API/src/Handlers/Delete/Feed/People/RequestHandler.php create mode 100644 API/src/Handlers/Delete/Post/CommandHandler.php create mode 100644 API/src/Handlers/Delete/Post/QueryHandler.php create mode 100644 API/src/Handlers/Delete/Post/RequestHandler.php create mode 100644 API/src/Handlers/Get/Collection/QueryHandler.php create mode 100644 API/src/Handlers/Get/Collection/RequestHandler.php create mode 100644 API/src/Handlers/Get/Feed/Invitations/QueryHandler.php create mode 100644 API/src/Handlers/Get/Feed/People/QueryHandler.php create mode 100644 API/src/Handlers/Get/Feed/People/RequestHandler.php create mode 100644 API/src/Handlers/Get/Feed/Posts/QueryHandler.php create mode 100644 API/src/Handlers/Get/Feed/Posts/RequestHandler.php create mode 100644 API/src/Handlers/Get/Feed/QueryHandler.php create mode 100644 API/src/Handlers/Get/Feed/RequestHandler.php create mode 100644 API/src/Handlers/Get/Feeds/QueryHandler.php create mode 100644 API/src/Handlers/Get/Index/QueryHandler.php create mode 100644 API/src/Handlers/Get/ListRequestHandler.php create mode 100644 API/src/Handlers/Get/Post/QueryHandler.php create mode 100644 API/src/Handlers/Get/Post/RequestHandler.php create mode 100644 API/src/Handlers/Get/Profile/QueryHandler.php create mode 100644 API/src/Handlers/Get/Profile/RequestHandler.php create mode 100644 API/src/Handlers/Get/Random/QueryHandler.php create mode 100644 API/src/Handlers/Get/Search/QueryHandler.php create mode 100644 API/src/Handlers/Get/Search/RequestHandler.php create mode 100644 API/src/Handlers/Get/User/Collections/QueryHandler.php create mode 100644 API/src/Handlers/Get/User/Feed/QueryHandler.php create mode 100644 API/src/Handlers/Get/User/Feeds/QueryHandler.php create mode 100644 API/src/Handlers/Get/User/QueryHandler.php create mode 100644 API/src/Handlers/Get/User/Todo/QueryHandler.php create mode 100644 API/src/Handlers/Get/User/Upcoming/QueryHandler.php create mode 100644 API/src/Handlers/Patch/Collection/CommandHandler.php create mode 100644 API/src/Handlers/Patch/Collection/QueryHandler.php create mode 100644 API/src/Handlers/Patch/Collection/RequestHandler.php create mode 100644 API/src/Handlers/Patch/Feed/CommandHandler.php create mode 100644 API/src/Handlers/Patch/Feed/Invitation/CommandHandler.php create mode 100644 API/src/Handlers/Patch/Feed/Invitation/QueryHandler.php create mode 100644 API/src/Handlers/Patch/Feed/Invitation/RequestHandler.php create mode 100644 API/src/Handlers/Patch/Feed/QueryHandler.php create mode 100644 API/src/Handlers/Patch/Feed/RequestHandler.php create mode 100644 API/src/Handlers/Patch/Feed/User/CommandHandler.php create mode 100644 API/src/Handlers/Patch/Feed/User/QueryHandler.php create mode 100644 API/src/Handlers/Patch/Feed/User/RequestHandler.php create mode 100644 API/src/Handlers/Patch/User/CommandHandler.php create mode 100644 API/src/Handlers/Patch/User/QueryHandler.php create mode 100644 API/src/Handlers/Patch/User/RequestHandler.php create mode 100644 API/src/Handlers/Post/Auth/CommandHandler.php create mode 100644 API/src/Handlers/Post/Auth/QueryHandler.php create mode 100644 API/src/Handlers/Post/Auth/RequestHandler.php create mode 100644 API/src/Handlers/Post/BetaRequest/CommandHandler.php create mode 100644 API/src/Handlers/Post/BetaRequest/QueryHandler.php create mode 100644 API/src/Handlers/Post/BetaRequest/RequestHandler.php create mode 100644 API/src/Handlers/Post/Collections/CommandHandler.php create mode 100644 API/src/Handlers/Post/Collections/CreateCollectionCommandHandler.php create mode 100644 API/src/Handlers/Post/Collections/RequestHandler.php create mode 100644 API/src/Handlers/Post/Feed/Follow/CommandHandler.php create mode 100644 API/src/Handlers/Post/Feed/Follow/QueryHandler.php create mode 100644 API/src/Handlers/Post/Feed/FollowRequestHandler.php create mode 100644 API/src/Handlers/Post/Feed/Invitations/CommandHandler.php create mode 100644 API/src/Handlers/Post/Feed/Invitations/QueryHandler.php create mode 100644 API/src/Handlers/Post/Feed/Invitations/RequestHandler.php create mode 100644 API/src/Handlers/Post/Feed/Unfollow/CommandHandler.php create mode 100644 API/src/Handlers/Post/Feed/Unfollow/QueryHandler.php create mode 100644 API/src/Handlers/Post/Feed/Upload/CommandHandler.php create mode 100644 API/src/Handlers/Post/Feed/Upload/RequestHandler.php create mode 100644 API/src/Handlers/Post/Feeds/CommandHandler.php create mode 100644 API/src/Handlers/Post/Feeds/RequestHandler.php create mode 100644 API/src/Handlers/Post/Posts/CommandHandler.php create mode 100644 API/src/Handlers/Post/Posts/QueryHandler.php create mode 100644 API/src/Handlers/Post/Posts/RequestHandler.php create mode 100644 API/src/Handlers/Post/Revoke/CommandHandler.php create mode 100644 API/src/Handlers/Post/Users/CommandHandler.php create mode 100644 API/src/Handlers/Post/Users/QueryHandler.php create mode 100644 API/src/Handlers/Post/Users/RequestHandler.php create mode 100644 API/src/Handlers/Post/Verify/CommandHandler.php create mode 100644 API/src/Handlers/Post/Verify/QueryHandler.php create mode 100644 API/src/Handlers/Post/Verify/RequestHandler.php create mode 100644 API/src/Handlers/Post/Verify/Resend/CommandHandler.php create mode 100644 API/src/Handlers/Post/Verify/Resend/QueryHandler.php create mode 100644 API/src/Handlers/Post/Verify/Resend/RequestHandler.php create mode 100644 API/src/Handlers/PostHandler.php create mode 100644 API/src/Handlers/PreHandler.php create mode 100644 API/src/Handlers/Put/User/CommandHandler.php create mode 100644 API/src/Handlers/Put/User/QueryHandler.php create mode 100644 API/src/Handlers/Put/User/RequestHandler.php create mode 100644 API/src/Handlers/QueryHandler.php create mode 100644 API/src/Handlers/RequestHandler.php create mode 100644 API/src/Handlers/ResponseHandler.php create mode 100644 API/src/Handlers/TransformationHandler.php create mode 100644 API/src/Locators/PostTypeLocator.php create mode 100644 API/src/Mappers/FeedUserMapper.php create mode 100644 API/src/Mappers/PostAttachmentMapper.php create mode 100644 API/src/Mappers/ResultsMapper.php create mode 100644 API/src/Mappers/SearchResultsMapper.php create mode 100644 API/src/Models/APIModel.php create mode 100644 API/src/Models/AuthModel.php create mode 100644 API/src/Models/BetaRequest/CreateModel.php create mode 100644 API/src/Models/Collection/CollectionModel.php create mode 100644 API/src/Models/Collection/CreateModel.php create mode 100644 API/src/Models/Collection/UpdateModel.php create mode 100644 API/src/Models/Feed/CreateModel.php create mode 100644 API/src/Models/Feed/FeedModel.php create mode 100644 API/src/Models/Feed/FollowModel.php create mode 100644 API/src/Models/Feed/Invitation/CreateModel.php create mode 100644 API/src/Models/Feed/Invitation/DeleteModel.php create mode 100644 API/src/Models/Feed/Invitation/UpdateModel.php create mode 100644 API/src/Models/Feed/People/DeleteModel.php create mode 100644 API/src/Models/Feed/People/ListModel.php create mode 100644 API/src/Models/Feed/Posts/ListModel.php create mode 100644 API/src/Models/Feed/UpdateModel.php create mode 100644 API/src/Models/Feed/UploadModel.php create mode 100644 API/src/Models/Feed/User/UpdateModel.php create mode 100644 API/src/Models/ListModel.php create mode 100644 API/src/Models/Post/CreateModel.php create mode 100644 API/src/Models/Post/PostModel.php create mode 100644 API/src/Models/Profile/ProfileModel.php create mode 100644 API/src/Models/SearchModel.php create mode 100644 API/src/Models/UpdateModelTrait.php create mode 100644 API/src/Models/User/CreateModel.php create mode 100644 API/src/Models/User/UpdateModel.php create mode 100644 API/src/Models/User/UpdatePasswordModel.php create mode 100644 API/src/Models/Verify/ResendModel.php create mode 100644 API/src/Models/Verify/VerifyModel.php create mode 100644 API/src/Queries/BetaRequest/FetchBetaRequestByEmailQuery.php create mode 100644 API/src/Queries/Feed/FeedExistsQuery.php create mode 100644 API/src/Queries/Feed/FetchFeedUserQuery.php create mode 100644 API/src/Queries/Feed/FetchFeedVanityQuery.php create mode 100644 API/src/Queries/Feed/FetchInvitationQuery.php create mode 100644 API/src/Queries/Feed/FetchInvitationsQuery.php create mode 100644 API/src/Queries/Feed/FetchVanityByNameQuery.php create mode 100644 API/src/Queries/Feed/InvitationExistsQuery.php create mode 100644 API/src/Queries/Feeds/FetchFeedQuery.php create mode 100644 API/src/Queries/Feeds/FetchFeedsQuery.php create mode 100644 API/src/Queries/Feeds/FetchFollowerQuery.php create mode 100644 API/src/Queries/Feeds/FetchPeopleQuery.php create mode 100644 API/src/Queries/Feeds/FetchPersonQuery.php create mode 100644 API/src/Queries/FetchCollectionQuery.php create mode 100644 API/src/Queries/File/FetchFileByPublicIdQuery.php create mode 100644 API/src/Queries/Post/FetchPostAttachmentsQuery.php create mode 100644 API/src/Queries/Post/FetchPostInfoQuery.php create mode 100644 API/src/Queries/Posts/FetchFeedPostsQuery.php create mode 100644 API/src/Queries/Posts/FetchPostQuery.php create mode 100644 API/src/Queries/Profile/FetchProfileQuery.php create mode 100644 API/src/Queries/SearchQuery.php create mode 100644 API/src/Queries/User/FetchAuthUserQuery.php create mode 100644 API/src/Queries/User/FetchTodoTasksQuery.php create mode 100644 API/src/Queries/User/FetchUpcomingEventsQuery.php create mode 100644 API/src/Queries/User/FetchUserByEmailQuery.php create mode 100644 API/src/Queries/User/FetchUserByIdQuery.php create mode 100644 API/src/Queries/User/FetchUserByUsernameQuery.php create mode 100644 API/src/Queries/User/FetchUserCollectionsQuery.php create mode 100644 API/src/Queries/User/FetchUserFeedQuery.php create mode 100644 API/src/Queries/User/FetchUserFeedsQuery.php create mode 100644 API/src/Queries/User/FetchUserPasswordQuery.php create mode 100644 API/src/Queries/User/FetchUsernameQuery.php create mode 100644 API/src/Queries/User/FetchVerificationTokenByEmail.php create mode 100644 API/src/Queries/User/FetchVerificationTokenQuery.php create mode 100644 API/src/Queries/User/IsInvitedQuery.php create mode 100644 API/src/Readers/RequestTokenReader.php create mode 100644 API/src/Routers/EndpointRouter.php create mode 100644 API/src/Services/BetaRequestService.php create mode 100644 API/src/Services/CollectionService.php create mode 100644 API/src/Services/FeedService.php create mode 100644 API/src/Services/FileService.php create mode 100644 API/src/Services/FollowerService.php create mode 100644 API/src/Services/PeopleService.php create mode 100644 API/src/Services/PostService.php create mode 100644 API/src/Services/UserService.php create mode 100644 API/src/ValueObjects/AccessToken.php create mode 100644 API/src/ValueObjects/Attachment.php create mode 100644 API/src/ValueObjects/BsonDateTime.php create mode 100644 API/src/ValueObjects/CollectionId.php create mode 100644 API/src/ValueObjects/CollectionName.php create mode 100644 API/src/ValueObjects/FeedDescription.php create mode 100644 API/src/ValueObjects/FeedFile.php create mode 100644 API/src/ValueObjects/FeedId.php create mode 100644 API/src/ValueObjects/FeedName.php create mode 100644 API/src/ValueObjects/FeedVanity.php create mode 100644 API/src/ValueObjects/FileToken.php create mode 100644 API/src/ValueObjects/Hash.php create mode 100644 API/src/ValueObjects/Pagination.php create mode 100644 API/src/ValueObjects/Password.php create mode 100644 API/src/ValueObjects/PostBody.php create mode 100644 API/src/ValueObjects/PostTitle.php create mode 100644 API/src/ValueObjects/StringBoolean.php create mode 100644 API/src/ValueObjects/UploadParams.php create mode 100644 API/src/ValueObjects/UserId.php create mode 100644 API/src/ValueObjects/Username.php create mode 100644 API/tests/bootstrap.php create mode 100644 Application/.babelrc create mode 100644 Application/.rollup.config.js create mode 100644 Application/Rakefile create mode 100644 Application/js/_stubs/dom4.js create mode 100644 Application/js/_stubs/encoding.js create mode 100644 Application/js/_stubs/events.js create mode 100644 Application/js/_stubs/object.js create mode 100644 Application/js/app/ajax.js create mode 100644 Application/js/application.js create mode 100644 Application/js/bootstrap/browser.js create mode 100644 Application/js/bootstrap/elements.js create mode 100644 Application/js/dom/custom-events.js create mode 100644 Application/js/dom/environment.js create mode 100644 Application/js/dom/fetch.js create mode 100644 Application/js/dom/form.js create mode 100644 Application/js/dom/next-render.js create mode 100644 Application/js/dom/string.js create mode 100644 Application/js/dom/toast.js create mode 100644 Application/js/dom/uploader.js create mode 100644 Application/js/elements/ajax-button.js create mode 100644 Application/js/elements/ajax-form.js create mode 100644 Application/js/elements/ajax-select.js create mode 100644 Application/js/elements/auto-textarea.js create mode 100644 Application/js/elements/file-drop.js create mode 100644 Application/js/elements/file-pick.js create mode 100644 Application/js/elements/file-upload.js create mode 100644 Application/js/elements/follow-button.js create mode 100644 Application/js/elements/form-error.js create mode 100644 Application/js/elements/form-field.js create mode 100644 Application/js/elements/paginated-list.js create mode 100644 Application/js/elements/paginated-view.js create mode 100644 Application/js/elements/pagination-button.js create mode 100644 Application/js/elements/post-attachment.js create mode 100644 Application/js/elements/time-elements.js create mode 100644 Application/js/elements/toast-message.js create mode 100644 Application/js/elements/validated-input.js create mode 100644 Application/js/event/signal.js create mode 100644 Application/polyfills/README.md create mode 100644 Application/polyfills/custom-elements.js create mode 100644 Application/polyfills/dom4.js create mode 100644 Application/polyfills/fetch.js create mode 100644 Application/polyfills/object.js create mode 100644 Framework/Rakefile create mode 100644 Framework/bootstrap.php create mode 100755 Framework/lib/Elasticsearch/Client.php create mode 100755 Framework/lib/Elasticsearch/ClientBuilder.php create mode 100755 Framework/lib/Elasticsearch/Common/EmptyLogger.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/AlreadyExpiredException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/BadMethodCallException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/BadRequest400Exception.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/ClientErrorResponseException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Conflict409Exception.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotConnectToHost.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotResolveHostException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Curl/OperationTimeoutException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/ElasticsearchException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Forbidden403Exception.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/InvalidArgumentException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/MaxRetriesException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Missing404Exception.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/NoDocumentsToGetException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/NoNodesAvailableException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/NoShardAvailableException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/RequestTimeout408Exception.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/RoutingMissingException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/RuntimeException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/ScriptLangNotSupportedException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/Serializer/JsonErrorException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/ServerErrorResponseException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/TransportException.php create mode 100755 Framework/lib/Elasticsearch/Common/Exceptions/UnexpectedValueException.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/AbstractConnectionPool.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/ConnectionPoolInterface.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/Selectors/RandomSelector.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/Selectors/RoundRobinSelector.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/Selectors/SelectorInterface.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/Selectors/StickyRoundRobinSelector.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/SimpleConnectionPool.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/SniffingConnectionPool.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/StaticConnectionPool.php create mode 100755 Framework/lib/Elasticsearch/ConnectionPool/StaticNoPingConnectionPool.php create mode 100755 Framework/lib/Elasticsearch/Connections/Connection.php create mode 100755 Framework/lib/Elasticsearch/Connections/ConnectionFactory.php create mode 100755 Framework/lib/Elasticsearch/Connections/ConnectionFactoryInterface.php create mode 100755 Framework/lib/Elasticsearch/Connections/ConnectionInterface.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/AbstractEndpoint.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Bulk.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/BulkEndpointInterface.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Aliases.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Allocation.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Count.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Fielddata.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Health.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Help.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Indices.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Master.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/NodeAttrs.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Nodes.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/PendingTasks.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Plugins.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Recovery.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Repositories.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Segments.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Shards.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Snapshots.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/Tasks.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cat/ThreadPool.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/ClearScroll.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/AllocationExplain.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Health.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/AbstractNodesEndpoint.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/HotThreads.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Info.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Shutdown.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Stats.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/PendingTasks.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Reroute.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/State.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Cluster/Stats.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Count.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/CountPercolate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Create.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Exists.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Explain.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/FieldStats.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Index.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Alias/AbstractAliasEndpoint.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Exists.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Update.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Analyze.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Cache/Clear.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/ClearCache.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Close.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Create.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Exists.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Exists/Types.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Field/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Flush.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/ForceMerge.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Gateway/Snapshot.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/GetField.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Open.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Recovery.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Refresh.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Rollover.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Seal.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Segments.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/ShardStores.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Shrink.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Snapshotindex.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Stats.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Status.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Template/AbstractTemplateEndpoint.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Template/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Template/Exists.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Template/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Template/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Type/Exists.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Post.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/Validate/Query.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Indices/ValidateQuery.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Info.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Ingest/Simulate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/MPercolate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/MTermVectors.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Mget.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Msearch.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Percolate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Ping.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Reindex.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/RenderSearchTemplate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Script/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Script/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Script/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Scroll.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Search.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/SearchShards.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/SearchTemplate.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Create.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Create.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Verify.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Restore.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Snapshot/Status.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Source/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Suggest.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Tasks/Cancel.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Tasks/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Tasks/TasksList.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Template/Delete.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Template/Get.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Template/Put.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/TermVectors.php create mode 100755 Framework/lib/Elasticsearch/Endpoints/Update.php create mode 100755 Framework/lib/Elasticsearch/Helper/Iterators/SearchHitIterator.php create mode 100755 Framework/lib/Elasticsearch/Helper/Iterators/SearchResponseIterator.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/AbstractNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/BooleanRequestWrapper.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/CatNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/ClusterNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/IndicesNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/IngestNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/NamespaceBuilderInterface.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/NodesNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/SnapshotNamespace.php create mode 100755 Framework/lib/Elasticsearch/Namespaces/TasksNamespace.php create mode 100755 Framework/lib/Elasticsearch/Serializers/ArrayToJSONSerializer.php create mode 100755 Framework/lib/Elasticsearch/Serializers/EverythingToJSONSerializer.php create mode 100755 Framework/lib/Elasticsearch/Serializers/SerializerInterface.php create mode 100755 Framework/lib/Elasticsearch/Serializers/SmartSerializer.php create mode 100755 Framework/lib/Elasticsearch/Transport.php create mode 100755 Framework/lib/Log/AbstractLogger.php create mode 100755 Framework/lib/Log/InvalidArgumentException.php create mode 100755 Framework/lib/Log/LogLevel.php create mode 100755 Framework/lib/Log/LoggerAwareInterface.php create mode 100755 Framework/lib/Log/LoggerAwareTrait.php create mode 100755 Framework/lib/Log/LoggerInterface.php create mode 100755 Framework/lib/Log/LoggerTrait.php create mode 100755 Framework/lib/Log/NullLogger.php create mode 100755 Framework/lib/Promise/CancellablePromiseInterface.php create mode 100755 Framework/lib/Promise/CancellationQueue.php create mode 100755 Framework/lib/Promise/Deferred.php create mode 100755 Framework/lib/Promise/Exception/LengthException.php create mode 100755 Framework/lib/Promise/ExtendedPromiseInterface.php create mode 100755 Framework/lib/Promise/FulfilledPromise.php create mode 100755 Framework/lib/Promise/LazyPromise.php create mode 100755 Framework/lib/Promise/Promise.php create mode 100755 Framework/lib/Promise/PromiseInterface.php create mode 100755 Framework/lib/Promise/PromisorInterface.php create mode 100755 Framework/lib/Promise/Queue/QueueInterface.php create mode 100755 Framework/lib/Promise/Queue/SynchronousQueue.php create mode 100755 Framework/lib/Promise/RejectedPromise.php create mode 100755 Framework/lib/Promise/UnhandledRejectionException.php create mode 100755 Framework/lib/Promise/functions.php create mode 100755 Framework/lib/Promise/functions_include.php create mode 100644 Framework/lib/README.md create mode 100755 Framework/lib/Ring/Client/ClientUtils.php create mode 100755 Framework/lib/Ring/Client/CurlFactory.php create mode 100755 Framework/lib/Ring/Client/CurlHandler.php create mode 100755 Framework/lib/Ring/Client/CurlMultiHandler.php create mode 100755 Framework/lib/Ring/Client/Middleware.php create mode 100755 Framework/lib/Ring/Client/MockHandler.php create mode 100755 Framework/lib/Ring/Client/StreamHandler.php create mode 100755 Framework/lib/Ring/Core.php create mode 100755 Framework/lib/Ring/Exception/CancelledException.php create mode 100755 Framework/lib/Ring/Exception/CancelledFutureAccessException.php create mode 100755 Framework/lib/Ring/Exception/ConnectException.php create mode 100755 Framework/lib/Ring/Exception/RingException.php create mode 100755 Framework/lib/Ring/Future/BaseFutureTrait.php create mode 100755 Framework/lib/Ring/Future/CompletedFutureArray.php create mode 100755 Framework/lib/Ring/Future/CompletedFutureValue.php create mode 100755 Framework/lib/Ring/Future/FutureArray.php create mode 100755 Framework/lib/Ring/Future/FutureArrayInterface.php create mode 100755 Framework/lib/Ring/Future/FutureInterface.php create mode 100755 Framework/lib/Ring/Future/FutureValue.php create mode 100755 Framework/lib/Ring/Future/MagicFutureTrait.php create mode 160000 Framework/lib/S3Helper create mode 100755 Framework/lib/Streams/AppendStream.php create mode 100755 Framework/lib/Streams/AsyncReadStream.php create mode 100755 Framework/lib/Streams/BufferStream.php create mode 100755 Framework/lib/Streams/CachingStream.php create mode 100755 Framework/lib/Streams/DroppingStream.php create mode 100755 Framework/lib/Streams/Exception/CannotAttachException.php create mode 100755 Framework/lib/Streams/Exception/SeekException.php create mode 100755 Framework/lib/Streams/FnStream.php create mode 100755 Framework/lib/Streams/GuzzleStreamWrapper.php create mode 100755 Framework/lib/Streams/InflateStream.php create mode 100755 Framework/lib/Streams/LazyOpenStream.php create mode 100755 Framework/lib/Streams/LimitStream.php create mode 100755 Framework/lib/Streams/MetadataStreamInterface.php create mode 100755 Framework/lib/Streams/NoSeekStream.php create mode 100755 Framework/lib/Streams/NullStream.php create mode 100755 Framework/lib/Streams/PumpStream.php create mode 100755 Framework/lib/Streams/Stream.php create mode 100755 Framework/lib/Streams/StreamDecoratorTrait.php create mode 100755 Framework/lib/Streams/StreamInterface.php create mode 100755 Framework/lib/Streams/Utils.php create mode 100644 Framework/phpunit.xml create mode 100644 Framework/src/Backends/AwsRestBackend.php create mode 100644 Framework/src/Backends/AwsS3Backend.php create mode 100644 Framework/src/Backends/DomBackend.php create mode 100644 Framework/src/Backends/ElasticBackend.php create mode 100644 Framework/src/Backends/FileBackend.php create mode 100644 Framework/src/Backends/InkBackend.php create mode 100644 Framework/src/Backends/MailBackendInterface.php create mode 100644 Framework/src/Backends/MailgunBackend.php create mode 100644 Framework/src/Backends/PostgresBackend.php create mode 100644 Framework/src/Backends/Streams/AbstractStreamWrapper.php create mode 100644 Framework/src/Bootstrap/AbstractBootstrapper.php create mode 100644 Framework/src/Configuration/Configuration.php create mode 100644 Framework/src/Configuration/ConfigurationInterface.php create mode 100644 Framework/src/Controllers/AbstractController.php create mode 100644 Framework/src/Controllers/ControllerInterface.php create mode 100644 Framework/src/Controllers/DeleteController.php create mode 100644 Framework/src/Controllers/GetController.php create mode 100644 Framework/src/Controllers/PatchController.php create mode 100644 Framework/src/Controllers/PostController.php create mode 100644 Framework/src/Controllers/PutController.php create mode 100644 Framework/src/Curl/Credentials/AbstractCredentials.php create mode 100644 Framework/src/Curl/Credentials/BasicAuth.php create mode 100644 Framework/src/Curl/Credentials/BearerToken.php create mode 100644 Framework/src/Curl/Credentials/CredentialsInterface.php create mode 100644 Framework/src/Curl/Curl.php create mode 100644 Framework/src/Curl/CurlHandler.php create mode 100644 Framework/src/Curl/RequestHeaders.php create mode 100644 Framework/src/Curl/RequestMethods/Delete.php create mode 100644 Framework/src/Curl/RequestMethods/Get.php create mode 100644 Framework/src/Curl/RequestMethods/Head.php create mode 100644 Framework/src/Curl/RequestMethods/Patch.php create mode 100644 Framework/src/Curl/RequestMethods/Post.php create mode 100644 Framework/src/Curl/RequestMethods/RequestMethodInterface.php create mode 100644 Framework/src/Curl/Response.php create mode 100644 Framework/src/DataStore/DataStoreInterface.php create mode 100644 Framework/src/DataStore/RedisBackend.php create mode 100644 Framework/src/Dom/Document.php create mode 100644 Framework/src/Dom/Element.php create mode 100644 Framework/src/Dom/Exception.php create mode 100644 Framework/src/Dom/Fragment.php create mode 100644 Framework/src/Dom/Node.php create mode 100644 Framework/src/ErrorHandlers/AbstractErrorHandler.php create mode 100644 Framework/src/Exceptions/FileUploadException.php create mode 100644 Framework/src/Exceptions/RouterException.php create mode 100644 Framework/src/Factories/AbstractChildFactory.php create mode 100644 Framework/src/Factories/BackendFactory.php create mode 100644 Framework/src/Factories/ChildFactoryInterface.php create mode 100644 Framework/src/Factories/FrameworkFactory.php create mode 100644 Framework/src/Factories/LoggerFactory.php create mode 100644 Framework/src/Factories/MasterFactory.php create mode 100644 Framework/src/Factories/MasterFactoryInterface.php create mode 100644 Framework/src/FrontController.php create mode 100644 Framework/src/Handlers/CommandHandlerInterface.php create mode 100644 Framework/src/Handlers/PostHandlerInterface.php create mode 100644 Framework/src/Handlers/PreHandlerInterface.php create mode 100644 Framework/src/Handlers/QueryHandlerInterface.php create mode 100644 Framework/src/Handlers/RequestHandlerInterface.php create mode 100644 Framework/src/Handlers/ResponseHandlerInterface.php create mode 100644 Framework/src/Handlers/TransformationHandlerInterface.php create mode 100644 Framework/src/Http/Headers/Authorization.php create mode 100644 Framework/src/Http/Redirect/AbstractRedirect.php create mode 100644 Framework/src/Http/Redirect/PermanentRedirect.php create mode 100644 Framework/src/Http/Redirect/RedirectInterface.php create mode 100644 Framework/src/Http/Redirect/TemporaryRedirect.php create mode 100644 Framework/src/Http/Request/AbstractRequest.php create mode 100644 Framework/src/Http/Request/AbstractWriteRequest.php create mode 100644 Framework/src/Http/Request/DeleteRequest.php create mode 100644 Framework/src/Http/Request/GetRequest.php create mode 100644 Framework/src/Http/Request/PatchRequest.php create mode 100644 Framework/src/Http/Request/PostRequest.php create mode 100644 Framework/src/Http/Request/PutRequest.php create mode 100644 Framework/src/Http/Request/RequestInterface.php create mode 100644 Framework/src/Http/Request/WriteRequestInterface.php create mode 100644 Framework/src/Http/Response/AbstractResponse.php create mode 100644 Framework/src/Http/Response/HtmlResponse.php create mode 100644 Framework/src/Http/Response/JsonResponse.php create mode 100644 Framework/src/Http/Response/ResponseInterface.php create mode 100644 Framework/src/Http/StatusCodes/BadRequest.php create mode 100644 Framework/src/Http/StatusCodes/Created.php create mode 100644 Framework/src/Http/StatusCodes/Forbidden.php create mode 100644 Framework/src/Http/StatusCodes/InternalServerError.php create mode 100644 Framework/src/Http/StatusCodes/MethodNotAllowed.php create mode 100644 Framework/src/Http/StatusCodes/MovedPermanently.php create mode 100644 Framework/src/Http/StatusCodes/NotFound.php create mode 100644 Framework/src/Http/StatusCodes/SeeOther.php create mode 100644 Framework/src/Http/StatusCodes/StatusCodeInterface.php create mode 100644 Framework/src/Http/StatusCodes/Unauthorized.php create mode 100644 Framework/src/Languages/English.php create mode 100644 Framework/src/Languages/German.php create mode 100644 Framework/src/Languages/LanguageInterface.php create mode 100644 Framework/src/Logging/LoggerAwareInterface.php create mode 100644 Framework/src/Logging/LoggerAwareTrait.php create mode 100644 Framework/src/Logging/Loggers/Logger.php create mode 100644 Framework/src/Logging/Loggers/LoggerInterface.php create mode 100644 Framework/src/Logging/Loggers/NsaLogger.php create mode 100644 Framework/src/Logging/Loggers/SlackLogger.php create mode 100644 Framework/src/Logging/Logs/AbstractLog.php create mode 100644 Framework/src/Logging/Logs/EmergencyLog.php create mode 100644 Framework/src/Logging/Logs/ErrorLog.php create mode 100644 Framework/src/Mails/MailInterface.php create mode 100644 Framework/src/Map/Map.php create mode 100644 Framework/src/Map/MapInterface.php create mode 100644 Framework/src/Map/ReadableMapInterface.php create mode 100644 Framework/src/Map/WritableMapInterface.php create mode 100644 Framework/src/Models/AbstractModel.php create mode 100644 Framework/src/Pdo/Value/Boolean.php create mode 100644 Framework/src/Pdo/Value/NullValue.php create mode 100644 Framework/src/Pdo/Value/ValueInterface.php create mode 100644 Framework/src/Routers/Router.php create mode 100644 Framework/src/Routers/RouterInterface.php create mode 100644 Framework/src/Translation/Gettext.php create mode 100644 Framework/src/Translation/TranslatorAwareInterface.php create mode 100644 Framework/src/Translation/TranslatorAwareTrait.php create mode 100644 Framework/src/Translation/TranslatorInterface.php create mode 100644 Framework/src/ValueObjects/Cookie.php create mode 100644 Framework/src/ValueObjects/EmailAddress.php create mode 100644 Framework/src/ValueObjects/EmailPerson.php create mode 100644 Framework/src/ValueObjects/Header.php create mode 100644 Framework/src/ValueObjects/InkResult.php create mode 100644 Framework/src/ValueObjects/StringDateTime.php create mode 100644 Framework/src/ValueObjects/Timestamp.php create mode 100644 Framework/src/ValueObjects/Token.php create mode 100644 Framework/src/ValueObjects/UploadedFile.php create mode 100644 Framework/src/ValueObjects/Uri.php create mode 100644 Framework/tests/bootstrap.php create mode 100644 Framework/tests/data/config.ini create mode 100644 Framework/tests/data/invalid-config.ini create mode 100644 Framework/tests/unit/Backends/FileBackendTest.php create mode 100644 Framework/tests/unit/Configuration/ConfigurationTest.php create mode 100644 Framework/tests/unit/Controllers/AbstractControllerTest.php create mode 100644 Framework/tests/unit/Curl/CurlTest.php create mode 100644 Framework/tests/unit/Curl/RequestMethods/RequestMethodsTest.php create mode 100644 Framework/tests/unit/Curl/ResponseTest.php create mode 100644 Framework/tests/unit/DataStore/RedisBackendTest.php create mode 100644 Framework/tests/unit/ErrorHandlers/AbstractErrorHandlerTest.php create mode 100644 Framework/tests/unit/Exceptions/FileUploadExceptionTest.php create mode 100644 Framework/tests/unit/Http/Redirect/AbstractRedirectTest.php create mode 100644 Framework/tests/unit/Http/Redirect/PermanentRedirectTest.php create mode 100644 Framework/tests/unit/Http/Redirect/TemporaryRedirectTest.php create mode 100644 Framework/tests/unit/Http/Request/PostRequestTest.php create mode 100644 Framework/tests/unit/Http/StatusCodes/StatusCodesTest.php create mode 100644 Framework/tests/unit/Languages/LanguageTest.php create mode 100644 Framework/tests/unit/Logging/LoggerAwareTraitTest.php create mode 100644 Framework/tests/unit/Logging/Loggers/LoggerTest.php create mode 100644 Framework/tests/unit/Logging/Loggers/SlackLoggerTest.php create mode 100644 Framework/tests/unit/Map/MapTest.php create mode 100644 Framework/tests/unit/ValueObjects/CookieTest.php create mode 100644 Framework/tests/unit/ValueObjects/EmailAddressTest.php create mode 100644 Framework/tests/unit/ValueObjects/EmailPersonTest.php create mode 100644 Framework/tests/unit/ValueObjects/HeaderTest.php create mode 100644 Framework/tests/unit/ValueObjects/TokenTest.php create mode 100644 Framework/tests/unit/ValueObjects/UriTest.php create mode 100644 Frontend/Rakefile create mode 100644 Frontend/bootstrap.php create mode 120000 Frontend/config/system.ini create mode 100644 Frontend/data/templates/content/tracking.html create mode 100644 Frontend/data/templates/content/verify/error.html create mode 100644 Frontend/data/templates/content/verify/success.html create mode 100644 Frontend/data/templates/template.html create mode 100644 Frontend/index.php create mode 120000 Frontend/public/css create mode 100644 Frontend/public/favicon.ico create mode 120000 Frontend/public/fonts create mode 120000 Frontend/public/icons create mode 100644 Frontend/public/images/logo.svg create mode 100644 Frontend/public/images/mono-logo.svg create mode 100644 Frontend/public/images/text-logo.svg create mode 100644 Frontend/public/images/triangle.svg create mode 120000 Frontend/public/js create mode 100755 Frontend/scripts/add-versions.sh create mode 100644 Frontend/src/Backends/ApiBackend.php create mode 100644 Frontend/src/Bootstrap/Bootstrapper.php create mode 100644 Frontend/src/Commands/AbstractApiCommand.php create mode 100644 Frontend/src/Commands/CreateBetaRequestCommand.php create mode 100644 Frontend/src/Commands/CreateFeedCommand.php create mode 100644 Frontend/src/Commands/CreateNoteCommand.php create mode 100644 Frontend/src/Commands/CreateUploadCommand.php create mode 100644 Frontend/src/Commands/DeleteFeedUserCommand.php create mode 100644 Frontend/src/Commands/DeletePostCommand.php create mode 100644 Frontend/src/Commands/Feed/DeleteFeedInvitationCommand.php create mode 100644 Frontend/src/Commands/Feed/FollowFeedCommand.php create mode 100644 Frontend/src/Commands/Feed/UnfollowFeedCommand.php create mode 100644 Frontend/src/Commands/Feed/UpdateFeedUserRoleCommand.php create mode 100644 Frontend/src/Commands/InviteFeedUserCommand.php create mode 100644 Frontend/src/Commands/LoginCommand.php create mode 100644 Frontend/src/Commands/LogoutCommand.php create mode 100644 Frontend/src/Commands/RegisterCommand.php create mode 100644 Frontend/src/Commands/ResendVerificationCommand.php create mode 100644 Frontend/src/Commands/VerifyCommand.php create mode 100644 Frontend/src/Commands/WriteSessionCommand.php create mode 100644 Frontend/src/DataObjects/ApiResponse.php create mode 100644 Frontend/src/DataObjects/User.php create mode 100644 Frontend/src/DataStore/DataStoreReader.php create mode 100644 Frontend/src/DataStore/DataStoreWriter.php create mode 100644 Frontend/src/ErrorHandlers/DevelopmentErrorHandler.php create mode 100644 Frontend/src/ErrorHandlers/ProductionErrorHandler.php create mode 100644 Frontend/src/Exceptions/AbstractException.php create mode 100644 Frontend/src/Exceptions/ApiException.php create mode 100644 Frontend/src/Exceptions/BadRequest.php create mode 100644 Frontend/src/Factories/ApplicationFactory.php create mode 100644 Frontend/src/Factories/CommandFactory.php create mode 100644 Frontend/src/Factories/ControllerFactory.php create mode 100644 Frontend/src/Factories/ErrorHandlerFactory.php create mode 100644 Frontend/src/Factories/FactoryTypeHint.php create mode 100644 Frontend/src/Factories/FactoryTypeHintTrait.php create mode 100644 Frontend/src/Factories/HandlerFactory.php create mode 100644 Frontend/src/Factories/LocatorFactory.php create mode 100644 Frontend/src/Factories/QueryFactory.php create mode 100644 Frontend/src/Factories/RendererFactory.php create mode 100644 Frontend/src/Factories/RouterFactory.php create mode 100644 Frontend/src/Factories/SessionFactory.php create mode 100644 Frontend/src/Factories/TransformationFactory.php create mode 100644 Frontend/src/Gateways/ApiGateway.php create mode 100644 Frontend/src/Handlers/CommandHandler.php create mode 100644 Frontend/src/Handlers/Get/Account/Verify/CommandHandler.php create mode 100644 Frontend/src/Handlers/Get/Account/Verify/RequestHandler.php create mode 100644 Frontend/src/Handlers/Get/CreatePostPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPeoplePage/CommandHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPeoplePage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPeoplePage/RequestHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPostsFragment/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedPostsFragment/RequestHandler.php create mode 100644 Frontend/src/Handlers/Get/FeedsPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/Fragment/TransformationHandler.php create mode 100644 Frontend/src/Handlers/Get/Homepage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/HomepagePostsFragment/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/HomepagePostsFragment/RequestHandler.php create mode 100644 Frontend/src/Handlers/Get/Page/PreHandler.php create mode 100644 Frontend/src/Handlers/Get/Page/TransformationHandler.php create mode 100644 Frontend/src/Handlers/Get/PostPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/SearchPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Get/SearchPage/RequestHandler.php create mode 100644 Frontend/src/Handlers/Get/StaticPage/QueryHandler.php create mode 100644 Frontend/src/Handlers/Post/CreateBetaRequest/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/CreateBetaRequest/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/CreateNote/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/CreateNote/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/DeleteFeedInvitation/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/DeleteFeedInvitation/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/DeleteFeedUser/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/DeleteFeedUser/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/DeletePost/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/DeletePost/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/Follow/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/Follow/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/InviteFeedUser/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/InviteFeedUser/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/Login/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/Login/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/Logout/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/NewFeed/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/NewFeed/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/PreHandler.php create mode 100644 Frontend/src/Handlers/Post/Register/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/Register/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/ResendVerification/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/ResendVerification/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/TransformationHandler.php create mode 100644 Frontend/src/Handlers/Post/Unfollow/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/UpdateFeedUserRole/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/UpdateFeedUserRole/RequestHandler.php create mode 100644 Frontend/src/Handlers/Post/Upload/CommandHandler.php create mode 100644 Frontend/src/Handlers/Post/Upload/RequestHandler.php create mode 100644 Frontend/src/Handlers/PostHandler.php create mode 100644 Frontend/src/Handlers/PreHandler.php create mode 100644 Frontend/src/Handlers/QueryHandler.php create mode 100644 Frontend/src/Handlers/RequestHandler.php create mode 100644 Frontend/src/Handlers/ResponseHandler.php create mode 100644 Frontend/src/Locators/SearchTabLocator.php create mode 100644 Frontend/src/Locators/StatusCodeLocator.php create mode 100644 Frontend/src/Models/Account/NewFeedModel.php create mode 100644 Frontend/src/Models/Account/VerifyModel.php create mode 100644 Frontend/src/Models/Action/CreateBetaRequestModel.php create mode 100644 Frontend/src/Models/Action/CreateNoteModel.php create mode 100644 Frontend/src/Models/Action/DeleteFeedUserModel.php create mode 100644 Frontend/src/Models/Action/DeletePostModel.php create mode 100644 Frontend/src/Models/Action/FollowModel.php create mode 100644 Frontend/src/Models/Action/InviteFeedUserModel.php create mode 100644 Frontend/src/Models/Action/LoginModel.php create mode 100644 Frontend/src/Models/Action/RegisterModel.php create mode 100644 Frontend/src/Models/Action/ResendVerificationModel.php create mode 100644 Frontend/src/Models/Action/UpdateFeedUserRoleModel.php create mode 100644 Frontend/src/Models/Action/UploadModel.php create mode 100644 Frontend/src/Models/ActionModel.php create mode 100644 Frontend/src/Models/CreatePostPageModel.php create mode 100644 Frontend/src/Models/FeedsPageModel.php create mode 100644 Frontend/src/Models/Fragment/FeedPostsFragmentModel.php create mode 100644 Frontend/src/Models/Fragment/HomepagePostsFragmentModel.php create mode 100644 Frontend/src/Models/FragmentModel.php create mode 100644 Frontend/src/Models/FrontendModel.php create mode 100644 Frontend/src/Models/HomepageModel.php create mode 100644 Frontend/src/Models/Page/FeedPageModel.php create mode 100644 Frontend/src/Models/Page/FeedPeoplePageModel.php create mode 100644 Frontend/src/Models/Page/FeedPostsPageModel.php create mode 100644 Frontend/src/Models/Page/SearchPageModel.php create mode 100644 Frontend/src/Models/PageModel.php create mode 100644 Frontend/src/Models/PostPageModel.php create mode 100644 Frontend/src/Models/StaticPageModel.php create mode 100644 Frontend/src/Queries/Feed/FetchFeedInvitationsQuery.php create mode 100644 Frontend/src/Queries/Feed/FetchFeedUsersQuery.php create mode 100644 Frontend/src/Queries/Feed/LookupVanityQuery.php create mode 100644 Frontend/src/Queries/FetchFeedPostsQuery.php create mode 100644 Frontend/src/Queries/FetchFeedQuery.php create mode 100644 Frontend/src/Queries/FetchStaticPageQuery.php create mode 100644 Frontend/src/Queries/FetchUserFeedQuery.php create mode 100644 Frontend/src/Queries/FetchUserFeedsQuery.php create mode 100644 Frontend/src/Queries/IsLoggedInQuery.php create mode 100644 Frontend/src/Queries/Post/FetchPostQuery.php create mode 100644 Frontend/src/Queries/SearchQuery.php create mode 100644 Frontend/src/Renderers/FeedPageRenderer.php create mode 100644 Frontend/src/Renderers/Fragment/FeedPostsFragmentRenderer.php create mode 100644 Frontend/src/Renderers/Fragment/FragmentRenderer.php create mode 100644 Frontend/src/Renderers/Fragment/HomepagePostsFragmentRenderer.php create mode 100644 Frontend/src/Renderers/Page/Account/VerifyAccountPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/CreatePostPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/Feed/FeedPeoplePageRenderer.php create mode 100644 Frontend/src/Renderers/Page/FeedPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/FeedsPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/HomepageRenderer.php create mode 100644 Frontend/src/Renderers/Page/PageRendererInterface.php create mode 100644 Frontend/src/Renderers/Page/PostPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/SearchPageRenderer.php create mode 100644 Frontend/src/Renderers/Page/StaticPageRenderer.php create mode 100644 Frontend/src/Renderers/PageRenderer.php create mode 100644 Frontend/src/Renderers/Renderer.php create mode 100644 Frontend/src/Renderers/Snippet/AjaxButtonSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedButtonsSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedCardSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedHeaderSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedInvitationBannerSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedInvitationCardSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedListSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedNavigationSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FeedUserCardSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/FloatingButtonSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/HomepageNavigationSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/HomepageOnboardingSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/IconButtonSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/IconSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/PaginationButtonSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/PostAttachmentSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/PostSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/SearchTabNavSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/TabNavSnippet.php create mode 100644 Frontend/src/Renderers/Snippet/UserRolesOptionsSnippet.php create mode 100644 Frontend/src/Routers/ActionRouter.php create mode 100644 Frontend/src/Routers/FeedPageRouter.php create mode 100644 Frontend/src/Routers/FragmentRouter.php create mode 100644 Frontend/src/Routers/NotFoundRouter.php create mode 100644 Frontend/src/Routers/PageRouter.php create mode 100644 Frontend/src/Routers/PostPageRouter.php create mode 100644 Frontend/src/Routers/StaticPageRouter.php create mode 100644 Frontend/src/Routers/UserActionRouter.php create mode 100644 Frontend/src/Routers/UserFragmentRouter.php create mode 100644 Frontend/src/Routers/UserPageRouter.php create mode 100644 Frontend/src/Session/Session.php create mode 100644 Frontend/src/TabNavItems/AbstractTabNavItem.php create mode 100644 Frontend/src/TabNavItems/FeedPage/People.php create mode 100644 Frontend/src/TabNavItems/FeedPage/Posts.php create mode 100644 Frontend/src/TabNavItems/FeedPage/Settings.php create mode 100644 Frontend/src/TabNavItems/Homepage/Feeds.php create mode 100644 Frontend/src/TabNavItems/Homepage/Posts.php create mode 100644 Frontend/src/TabNavItems/SearchPage/Everything.php create mode 100644 Frontend/src/TabNavItems/SearchPage/Feeds.php create mode 100644 Frontend/src/TabNavItems/SearchPage/Posts.php create mode 100644 Frontend/src/TabNavItems/TabNavItem.php create mode 100644 Frontend/src/Tabs/FeedPage/People.php create mode 100644 Frontend/src/Tabs/FeedPage/Posts.php create mode 100644 Frontend/src/Tabs/FeedPage/Settings.php create mode 100644 Frontend/src/Tabs/Homepage/Feeds.php create mode 100644 Frontend/src/Tabs/Homepage/Posts.php create mode 100644 Frontend/src/Tabs/SearchPage/Everything.php create mode 100644 Frontend/src/Tabs/SearchPage/Feeds.php create mode 100644 Frontend/src/Tabs/SearchPage/Posts.php create mode 100644 Frontend/src/Tabs/Tab.php create mode 100644 Frontend/src/Transformations/CanonicalUriTransformation.php create mode 100644 Frontend/src/Transformations/CsrfTokenTransformation.php create mode 100644 Frontend/src/Transformations/TitleTransformation.php create mode 100644 Frontend/src/Transformations/TrackingTransformation.php create mode 100644 Frontend/src/Transformations/Transformer.php create mode 100644 Frontend/src/Transformations/UserDropdownTransformation.php create mode 100644 Frontend/src/ValueObjects/Feed.php create mode 100644 Frontend/src/ValueObjects/PaginatedResult.php create mode 160000 Ink create mode 100644 LICENSE create mode 100644 Library/Rakefile create mode 100644 Library/bootstrap.php create mode 100644 Library/src/Backends/Streams/DataStreamWrapper.php create mode 100644 Library/src/Backends/Streams/PagesStreamWrapper.php create mode 100644 Library/src/Backends/Streams/TemplatesStreamWrapper.php create mode 100644 Library/src/Builders/UriBuilder.php create mode 100644 Library/src/DataObjects/FeedInvitation.php create mode 100644 Library/src/DataObjects/StaticPage.php create mode 100644 Library/src/DataStore/AbstractDataStoreReader.php create mode 100644 Library/src/DataStore/AbstractDataStoreWriter.php create mode 100644 Library/src/Factories/ApplicationFactory.php create mode 100644 Library/src/Factories/IndexerFactory.php create mode 100644 Library/src/Factories/LocatorFactory.php create mode 100644 Library/src/Factories/MapperFactory.php create mode 100644 Library/src/Factories/ServiceFactory.php create mode 100644 Library/src/Indexers/FeedIndexer.php create mode 100644 Library/src/Indexers/Indexer.php create mode 100644 Library/src/Indexers/PostIndexer.php create mode 100644 Library/src/Indexers/UserIndexer.php create mode 100644 Library/src/Locators/SearchTypeLocator.php create mode 100644 Library/src/Locators/UserRoleLocator.php create mode 100644 Library/src/Mappers/DocumentMapper.php create mode 100644 Library/src/Mappers/FeedMapper.php create mode 100644 Library/src/Mappers/PostMapper.php create mode 100644 Library/src/PostTypes/Event.php create mode 100644 Library/src/PostTypes/Note.php create mode 100644 Library/src/PostTypes/PostTypeInterface.php create mode 100644 Library/src/PostTypes/Task.php create mode 100644 Library/src/SearchTypes/All.php create mode 100644 Library/src/SearchTypes/Feed.php create mode 100644 Library/src/SearchTypes/Post.php create mode 100644 Library/src/SearchTypes/SearchType.php create mode 100644 Library/src/Services/FeedInvitationService.php create mode 100644 Library/src/TaskPriorities/High.php create mode 100644 Library/src/TaskPriorities/Low.php create mode 100644 Library/src/TaskPriorities/Normal.php create mode 100644 Library/src/TaskPriorities/Priority.php create mode 100644 Library/src/TaskPriorities/Random.php create mode 100644 Library/src/Tasks/BuildFeedTask.php create mode 100644 Library/src/Tasks/BuildFeedsTask.php create mode 100644 Library/src/Tasks/BuildPostTask.php create mode 100644 Library/src/Tasks/BuildPostsTask.php create mode 100644 Library/src/Tasks/BuildStaticPagesTask.php create mode 100644 Library/src/Tasks/DeleteUnusedFilesTask.php create mode 100644 Library/src/Tasks/IndexFeedTask.php create mode 100644 Library/src/Tasks/IndexFeedsTask.php create mode 100644 Library/src/Tasks/IndexPostTask.php create mode 100644 Library/src/Tasks/IndexPostsTask.php create mode 100644 Library/src/Tasks/IndexUserTask.php create mode 100644 Library/src/Tasks/IndexUsersTask.php create mode 100644 Library/src/Tasks/InitialTask.php create mode 100644 Library/src/Tasks/SendFeedInvitationTask.php create mode 100644 Library/src/Tasks/SendVerificationEmailTask.php create mode 100644 Library/src/Tasks/TaskInterface.php create mode 100644 Library/src/Transformations/TransformationInterface.php create mode 100644 Library/src/Transformations/TranslateTransformation.php create mode 100644 Library/src/UserRoles/DefaultUserRole.php create mode 100644 Library/src/UserRoles/Moderator.php create mode 100644 Library/src/UserRoles/Owner.php create mode 100644 Library/src/UserRoles/UserRole.php create mode 100644 Library/src/ValueObjects/DisplayName.php create mode 100644 Locale/Rakefile create mode 100644 Locale/de_CH/LC_MESSAGES/messages.po create mode 100644 Locale/en_GB/LC_MESSAGES/messages.po create mode 100644 README.md create mode 100644 Rakefile create mode 120000 Showcase/css create mode 120000 Showcase/favicon.ico create mode 100644 Showcase/feed.html create mode 120000 Showcase/icons create mode 120000 Showcase/images create mode 100644 Showcase/index.html create mode 120000 Showcase/js create mode 100644 Showcase/new-post.html create mode 100644 Styles/README.md create mode 100644 Styles/Rakefile create mode 100644 Styles/browserslist create mode 100644 Styles/fonts/README.md create mode 100644 Styles/fonts/vollkorn-medium-webfont.woff create mode 100644 Styles/icons/README.md create mode 100644 Styles/icons/actions/delete.svg create mode 100644 Styles/icons/actions/invite.svg create mode 100644 Styles/icons/actions/post.svg create mode 100644 Styles/icons/attachment.svg create mode 100644 Styles/icons/done.svg create mode 100644 Styles/icons/event.svg create mode 100644 Styles/icons/feed.svg create mode 100644 Styles/icons/label.svg create mode 100644 Styles/icons/note.svg create mode 100644 Styles/icons/options.svg create mode 100644 Styles/icons/person.svg create mode 100644 Styles/icons/private.svg create mode 100644 Styles/icons/public.svg create mode 100644 Styles/icons/search.svg create mode 100644 Styles/icons/settings.svg create mode 100644 Styles/icons/task.svg create mode 100644 Styles/icons/twitter.svg create mode 100644 Styles/icons/verified.svg create mode 100644 Styles/less/_helpers.less create mode 100644 Styles/less/_mixins.less create mode 100644 Styles/less/_variables.less create mode 100644 Styles/less/application.less create mode 100644 Styles/less/components/all.less create mode 100644 Styles/less/components/basic/all.less create mode 100644 Styles/less/components/basic/basic-button.less create mode 100644 Styles/less/components/basic/basic-heading-a.less create mode 100644 Styles/less/components/basic/basic-heading-b.less create mode 100644 Styles/less/components/basic/basic-icon.less create mode 100644 Styles/less/components/basic/basic-input.less create mode 100644 Styles/less/components/basic/basic-link.less create mode 100644 Styles/less/components/basic/basic-paragraph.less create mode 100644 Styles/less/components/basic/basic-pre.less create mode 100644 Styles/less/components/feed-list.less create mode 100644 Styles/less/components/feed/all.less create mode 100644 Styles/less/components/feed/feed-card.less create mode 100644 Styles/less/components/feed/feed-header.less create mode 100644 Styles/less/components/flex-container.less create mode 100644 Styles/less/components/floating/all.less create mode 100644 Styles/less/components/floating/floating-button.less create mode 100644 Styles/less/components/floating/floating-buttons.less create mode 100644 Styles/less/components/form/all.less create mode 100644 Styles/less/components/form/form-box.less create mode 100644 Styles/less/components/form/form-checkbox.less create mode 100644 Styles/less/components/form/form-error.less create mode 100644 Styles/less/components/form/form-field.less create mode 100644 Styles/less/components/generic-card.less create mode 100644 Styles/less/components/light/all.less create mode 100644 Styles/less/components/light/light-button.less create mode 100644 Styles/less/components/light/light-pill.less create mode 100644 Styles/less/components/light/light-select.less create mode 100644 Styles/less/components/logo-link.less create mode 100644 Styles/less/components/page/all.less create mode 100644 Styles/less/components/page/page-banner.less create mode 100644 Styles/less/components/page/page-footer.less create mode 100644 Styles/less/components/page/page-header.less create mode 100644 Styles/less/components/page/page-wrapper.less create mode 100644 Styles/less/components/pagination-button.less create mode 100644 Styles/less/components/post/all.less create mode 100644 Styles/less/components/post/post-attachment.less create mode 100644 Styles/less/components/post/post-card-outside-text.less create mode 100644 Styles/less/components/post/post-card.less create mode 100644 Styles/less/components/post/post-content.less create mode 100644 Styles/less/components/post/post-list.less create mode 100644 Styles/less/components/search/all.less create mode 100644 Styles/less/components/search/search-form.less create mode 100644 Styles/less/components/survey/all.less create mode 100644 Styles/less/components/survey/survey-question-card.less create mode 100644 Styles/less/components/tab-nav.less create mode 100644 Styles/less/components/task/all.less create mode 100644 Styles/less/components/task/task-checkbox.less create mode 100644 Styles/less/components/toast/all.less create mode 100644 Styles/less/components/toast/toast-container.less create mode 100644 Styles/less/components/toast/toast-message.less create mode 100644 Styles/less/components/user/all.less create mode 100644 Styles/less/components/user/user-list-item.less create mode 100644 Styles/less/components/user/user-list.less create mode 100644 Styles/less/core/all.less create mode 100644 Styles/less/core/basics.less create mode 100644 Styles/less/core/normalize.less create mode 100644 Styles/less/core/reset.less create mode 100644 Styles/less/core/typography.less create mode 100644 Styles/less/elements/all.less create mode 100644 Styles/less/elements/form-error.less create mode 100644 Styles/less/fonts/vollkorn.less create mode 100644 Survey/Rakefile create mode 100644 Survey/bootstrap.php create mode 120000 Survey/config/system.ini create mode 100644 Survey/data/templates/template.html create mode 100644 Survey/index.php create mode 100644 Survey/src/Bootstrap/Bootstrapper.php create mode 100644 Survey/src/Builders/UriBuilder.php create mode 100644 Survey/src/Commands/ApproveBetaRequestCommand.php create mode 100644 Survey/src/Commands/InsertAnswerCommand.php create mode 100644 Survey/src/DataObjects/Question.php create mode 100644 Survey/src/Factories/ApplicationFactory.php create mode 100644 Survey/src/Factories/CommandFactory.php create mode 100644 Survey/src/Factories/ControllerFactory.php create mode 100644 Survey/src/Factories/HandlerFactory.php create mode 100644 Survey/src/Factories/QueryFactory.php create mode 100644 Survey/src/Factories/RendererFactory.php create mode 100644 Survey/src/Factories/RouterFactory.php create mode 100644 Survey/src/Handlers/Get/SurveyPage/QueryHandler.php create mode 100644 Survey/src/Handlers/Post/Survey/CommandHandler.php create mode 100644 Survey/src/Handlers/Post/Survey/QueryHandler.php create mode 100644 Survey/src/Handlers/Post/Survey/RequestHandler.php create mode 100644 Survey/src/Models/Action/SurveyActionModel.php create mode 100644 Survey/src/Models/Page/SurveyPageModel.php create mode 100644 Survey/src/Queries/FetchBetaRequestQuery.php create mode 100644 Survey/src/Queries/FetchQuestionsQuery.php create mode 100644 Survey/src/Renderers/PageContent/SurveyPageContentRenderer.php create mode 100644 Survey/src/Routers/ActionRouter.php create mode 100644 Survey/src/Routers/BetaSurveyRouter.php create mode 100644 Survey/src/ValueObjects/AnswerValue.php create mode 100644 Worker/Rakefile create mode 100644 Worker/bootstrap.php create mode 120000 Worker/config/system.ini create mode 100644 Worker/data/mails/invitation.xhtml create mode 100644 Worker/data/mails/verification.xhtml create mode 100644 Worker/data/pages/404.json create mode 100644 Worker/data/pages/beta-thanks.json create mode 100644 Worker/data/pages/beta.json create mode 100644 Worker/data/pages/create-feed.json create mode 100644 Worker/data/pages/error.json create mode 100644 Worker/data/pages/homepage.json create mode 100644 Worker/data/pages/login.json create mode 100644 Worker/data/pages/register-confirmation.json create mode 100644 Worker/data/pages/register.json create mode 100644 Worker/data/pages/resend-verification.json create mode 100644 Worker/data/pages/survey-thanks.json create mode 100644 Worker/data/routes.json create mode 100644 Worker/data/templates/content/404.html create mode 100644 Worker/data/templates/content/account/create-feed.html create mode 100644 Worker/data/templates/content/beta/request.html create mode 100644 Worker/data/templates/content/beta/thanks.html create mode 100644 Worker/data/templates/content/error.html create mode 100644 Worker/data/templates/content/homepage.html create mode 100644 Worker/data/templates/content/login.html create mode 100644 Worker/data/templates/content/register-confirmation.html create mode 100644 Worker/data/templates/content/register.html create mode 100644 Worker/data/templates/content/resend-verification.html create mode 100644 Worker/data/templates/content/survey/thanks.html create mode 100755 Worker/push.php create mode 100644 Worker/src/Bootstrapper.php create mode 100644 Worker/src/Builders/StaticPageBuilder.php create mode 100644 Worker/src/DataStore/DataStoreReader.php create mode 100644 Worker/src/DataStore/DataStoreWriter.php create mode 100644 Worker/src/Factories/ApplicationFactory.php create mode 100644 Worker/src/Factories/LocatorFactory.php create mode 100644 Worker/src/Factories/MailFactory.php create mode 100644 Worker/src/Factories/RendererFactory.php create mode 100644 Worker/src/Factories/RunnerFactory.php create mode 100644 Worker/src/Locators/RunnerLocator.php create mode 100644 Worker/src/Locators/StatusCodeLocator.php create mode 100644 Worker/src/Locators/TaskLocator.php create mode 100644 Worker/src/Mails/AbstractMail.php create mode 100644 Worker/src/Mails/FeedInvitationMail.php create mode 100644 Worker/src/Mails/VerificationMail.php create mode 100644 Worker/src/Renderers/StaticPageRenderer.php create mode 100644 Worker/src/Runners/BuildFeedRunner.php create mode 100644 Worker/src/Runners/BuildFeedsRunner.php create mode 100644 Worker/src/Runners/BuildPostRunner.php create mode 100644 Worker/src/Runners/BuildPostsRunner.php create mode 100644 Worker/src/Runners/BuildStaticPagesRunner.php create mode 100644 Worker/src/Runners/DeleteUnusedFilesRunner.php create mode 100644 Worker/src/Runners/IndexFeedRunner.php create mode 100644 Worker/src/Runners/IndexFeedsRunner.php create mode 100644 Worker/src/Runners/IndexPostRunner.php create mode 100644 Worker/src/Runners/IndexPostsRunner.php create mode 100644 Worker/src/Runners/IndexUserRunner.php create mode 100644 Worker/src/Runners/IndexUsersRunner.php create mode 100644 Worker/src/Runners/InitialRunner.php create mode 100644 Worker/src/Runners/RunnerInterface.php create mode 100644 Worker/src/Runners/SendFeedInvitationRunner.php create mode 100644 Worker/src/Runners/SendVerificationEmailRunner.php create mode 100644 Worker/src/Services/FeedService.php create mode 100644 Worker/src/Services/FileService.php create mode 100644 Worker/src/Services/PostService.php create mode 100644 Worker/src/Services/UserService.php create mode 100644 Worker/src/Transformations/TransformationInterface.php create mode 100644 Worker/src/Transformations/TranslateTransformation.php create mode 100644 Worker/src/Worker.php create mode 100755 Worker/worker.php create mode 100644 containers/ttio-api/Dockerfile create mode 100644 containers/ttio-dev-frontend/Dockerfile create mode 100644 containers/ttio-dev-frontend/ttio-root-ca.crt create mode 100644 containers/ttio-dev-proxy/Dockerfile create mode 100644 containers/ttio-dev-proxy/certs/fullchain.pem create mode 100644 containers/ttio-dev-proxy/certs/privkey.pem create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/beta.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/coverage.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/dev.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/devapi.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/showcase.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/conf.d/survey.timetab.io.conf create mode 100644 containers/ttio-dev-proxy/nginx/nginx.conf create mode 100644 containers/ttio-dev-survey/Dockerfile create mode 100644 containers/ttio-dev-survey/ttio-root-ca.crt create mode 100644 containers/ttio-frontend/Dockerfile create mode 100644 containers/ttio-postgres/patches/003-survey.sql create mode 100644 containers/ttio-proxy/Dockerfile create mode 100644 containers/ttio-proxy/config/nginx/conf.d/api.timetab.io.conf create mode 100644 containers/ttio-proxy/config/nginx/conf.d/beta.timetab.io.conf create mode 100644 containers/ttio-proxy/config/nginx/conf.d/default.conf create mode 100644 containers/ttio-proxy/config/nginx/conf.d/docker.ttio.cloud.conf create mode 100644 containers/ttio-proxy/config/nginx/conf.d/survey.timetab.io.conf create mode 100644 containers/ttio-proxy/config/nginx/conf.d/timetab.io.conf create mode 100644 containers/ttio-proxy/config/nginx/nginx.conf create mode 100644 containers/ttio-proxy/config/nginx/ssl_config create mode 100644 containers/ttio-staging/Dockerfile create mode 100644 containers/ttio-staging/config/docker.repo create mode 100644 containers/ttio-survey/Dockerfile create mode 100644 containers/ttio-worker/Dockerfile create mode 100644 data/elastic-mappings.json create mode 100644 data/patches/001-feed-vanity-update.sql create mode 100644 data/patches/002-feed-description.sql create mode 100644 data/schema.sql create mode 100644 packages/ttio-docker-registry/auth/htpasswd create mode 100644 packages/ttio-docker-registry/package.spec create mode 100644 packages/ttio-docker-registry/units/ttio-docker-registry.service create mode 100644 packages/ttio-server/config/letsencrypt/docker.ttio.cloud.ini create mode 100644 packages/ttio-server/config/letsencrypt/timetab.io.ini create mode 100755 packages/ttio-server/config/renew-certs.sh create mode 100644 packages/ttio-server/cron.d/renew-certs create mode 100644 packages/ttio-server/package.spec create mode 100644 packages/ttio-web/cron.d/run-tasks create mode 100644 packages/ttio-web/package.spec create mode 100644 packages/ttio-web/units/ttio-api.service create mode 100644 packages/ttio-web/units/ttio-elastic.service create mode 100644 packages/ttio-web/units/ttio-frontend.service create mode 100644 packages/ttio-web/units/ttio-postgres.service create mode 100644 packages/ttio-web/units/ttio-proxy.service create mode 100644 packages/ttio-web/units/ttio-redis.service create mode 100644 packages/ttio-web/units/ttio-survey.service create mode 100644 packages/ttio-web/units/ttio-web.target create mode 100644 packages/ttio-web/units/ttio-worker@.service create mode 100644 packages/ttio-web/units/ttio-workers.target create mode 100644 rake/gen_autoload.rb create mode 100755 scripts/attach-workers.sh create mode 100755 scripts/build-containers.sh create mode 100755 scripts/build-dev-containers.sh create mode 100755 scripts/build-packages.sh create mode 100755 scripts/create-icon.php create mode 100755 scripts/deploy.sh create mode 100755 scripts/pull-containers.sh create mode 100755 scripts/push-containers.sh create mode 100755 scripts/push-task.sh create mode 100755 scripts/rake.sh create mode 100755 scripts/release-packages.sh create mode 100755 scripts/resty-auth.sh create mode 100755 scripts/setup.sh create mode 100755 scripts/spawn-workers.sh create mode 100755 scripts/staging-release.sh create mode 100755 scripts/start-the-magic.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e69e7d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +persistent +scripts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4c63f57 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[*.{php,js,css,less,json,html,sh}] +indent_style = space +trim_trailing_whitespace = true + +[*.php] +indent_size = 4 + +[*.{js,css,less,json,sh,service,target,ini}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f720fdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +autoload.php +*.mo +/Framework/build/ +/API/build/ +/Styles/css/ +/Application/build/ +/packages/**/rpm/ +/.idea +/persistent/ +npm-debug.log +/config diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2232e85 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "Ink"] + path = Ink + url = git@github.com:timetabio/Ink + branch = master +[submodule "Framework/lib/S3Helper"] + path = Framework/lib/S3Helper + url = git@github.com:timetabio/S3Helper.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b32ea6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +sudo: required +dist: trusty +group: edge + +services: +- docker + +env: +- TTIO_BUILD_ENV=production + +notifications: + email: false + +before_install: +- 'openssl aes-256-cbc -K $encrypted_b3a2ec95562f_key -iv $encrypted_b3a2ec95562f_iv -in data/timetabio-bot.enc -out data/timetabio-bot -d' +- docker login -p ${TTIO_DOCKER_PASSWORD} -u bot https://docker.ttio.cloud:5000 + +addons: + ssh_known_hosts: + - timetab.io + +before_script: +- ./scripts/rake.sh + +script: +- ./scripts/rake.sh test + +before_deploy: +- chmod 600 $TRAVIS_BUILD_DIR/data/timetabio-bot +- ./scripts/ + +deploy: + provider: script + skip_cleanup: true + + script: ./scripts/deploy.sh + + on: + tags: true diff --git a/API/Rakefile b/API/Rakefile new file mode 100644 index 0000000..6b41416 --- /dev/null +++ b/API/Rakefile @@ -0,0 +1,19 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src'), + # gen_autoload('tests') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # sh 'phpunit' +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/API/bootstrap.php b/API/bootstrap.php new file mode 100644 index 0000000..83a7cfc --- /dev/null +++ b/API/bootstrap.php @@ -0,0 +1,4 @@ +run(); +} diff --git a/API/phpunit.xml b/API/phpunit.xml new file mode 100644 index 0000000..13fcbed --- /dev/null +++ b/API/phpunit.xml @@ -0,0 +1,35 @@ + + + + + + tests + + + + + + + + + + src + + src/autoload.php + + + + diff --git a/API/scripts/create-feeds.sh b/API/scripts/create-feeds.sh new file mode 100755 index 0000000..71520ad --- /dev/null +++ b/API/scripts/create-feeds.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +WORDS=$(curl "https://raw.githubusercontent.com/hzlzh/Domain-Name-List/master/Animal-words.txt") +API_BASE='https://devapi.timetab.io/v1' + +TOKEN_JSON=$(curl -X POST ${API_BASE}/auth -d user='peanut_butter' -d password='foo_bar_baz' -d scopes='*') +TOKEN=$(echo ${TOKEN_JSON} | python -c "import json, sys; obj = json.load(sys.stdin); print obj['access_token'];") + +for WORD in ${WORDS}; do + curl -X POST ${API_BASE}/feeds -H "Authorization: Bearer ${TOKEN}" -d name=${WORD} -d is_private=false +done diff --git a/API/scripts/create-system-token.php b/API/scripts/create-system-token.php new file mode 100755 index 0000000..1577269 --- /dev/null +++ b/API/scripts/create-system-token.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +connect($config['redisHost']); + + if ($redis->exists('system_token')) { + echo 'System token already exists.' . PHP_EOL; + exit; + } + + $redis->set('access_token_' . $token, serialize($accessToken)); + $redis->set('system_token', (string) $token); + + echo 'System token created.' . PHP_EOL; +} diff --git a/API/scripts/create-user.sh b/API/scripts/create-user.sh new file mode 100755 index 0000000..8f0e09b --- /dev/null +++ b/API/scripts/create-user.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +TOKEN=$(docker exec ttio-dev-redis redis-cli --raw GET system_token) +API_BASE='https://devapi.timetab.io/v1' + +printf 'Email [peanutbutter@timetab.io]: ' +read EMAIL + +printf 'Username [peanut_butter]: ' +read USERNAME + +printf 'Password [foo_bar_baz]: ' +read -s PASSWORD + +echo "" + +if [ -z "${EMAIL}" ]; then + EMAIL='peanutbutter@timetab.io' +fi + +if [ -z "${USERNAME}" ]; then + USERNAME='peanut_butter' +fi + +if [ -z "${PASSWORD}" ]; then + PASSWORD='foo_bar_baz' +fi + +curl -X POST ${API_BASE}/users -H "Authorization: Bearer ${TOKEN}" -d username="${USERNAME}" -d password="${PASSWORD}" -d email="${EMAIL}" + +VERIFY_TOKEN=($(docker exec -i ttio-dev-postgres psql -U postgres -t -q -c "SELECT token FROM verification_tokens WHERE email='${EMAIL}' LIMIT 1")) + +curl -X POST ${API_BASE}/verify -H "Authorization: Bearer ${TOKEN}" -d token=${VERIFY_TOKEN} diff --git a/API/src/Access/AccessControl.php b/API/src/Access/AccessControl.php new file mode 100644 index 0000000..c63aa4a --- /dev/null +++ b/API/src/Access/AccessControl.php @@ -0,0 +1,57 @@ +dataStoreReader = $dataStoreReader; + $this->requestTokenReader = $requestTokenReader; + } + + public function hasAccess(RequestInterface $request, EndpointInterface $endpoint): bool + { + return $endpoint->hasAccess($this->getAccessType($request)); + } + + protected function getAccessType(RequestInterface $request): AccessTypeInterface + { + $token = $this->requestTokenReader->read($request); + + if ($token === null) { + return new NoAccess; + } + + if (!$this->dataStoreReader->hasAccessToken($token)) { + return new NoAccess; + } + + $accessToken = $this->dataStoreReader->getAccessToken($token); + + return $accessToken->getAccessType(); + } + } +} diff --git a/API/src/Access/AccessControl/AbstractAccessControl.php b/API/src/Access/AccessControl/AbstractAccessControl.php new file mode 100644 index 0000000..84129a0 --- /dev/null +++ b/API/src/Access/AccessControl/AbstractAccessControl.php @@ -0,0 +1,28 @@ +getAccessType(); + + if ($accessType instanceof FullAccess) { + return true; + } + + if ($accessType instanceof ScopedAccess) { + return $accessType->hasScope($scope); + } + + return false; + } + } +} diff --git a/API/src/Access/AccessControl/CollectionAccessControl.php b/API/src/Access/AccessControl/CollectionAccessControl.php new file mode 100644 index 0000000..52de86a --- /dev/null +++ b/API/src/Access/AccessControl/CollectionAccessControl.php @@ -0,0 +1,33 @@ +getUserId(); + + if ($collection['owner_id'] === (string) $userId) { + return $this->checkScope($accessToken, 'collections:read'); + } + + return false; + } + + public function hasWriteAccess(AccessToken $accessToken, array $collection): bool + { + $userId = (string) $accessToken->getUserId(); + + if ($collection['owner_id'] === (string) $userId) { + return $this->checkScope($accessToken, 'collections:write'); + } + + return false; + } + } +} diff --git a/API/src/Access/AccessControl/FeedAccessControl.php b/API/src/Access/AccessControl/FeedAccessControl.php new file mode 100644 index 0000000..fb07867 --- /dev/null +++ b/API/src/Access/AccessControl/FeedAccessControl.php @@ -0,0 +1,130 @@ +dataStoreReader = $dataStoreReader; + } + + public function hasReadAccess(string $feedId, AccessToken $accessToken = null): bool + { + if (!$this->dataStoreReader->hasFeed($feedId)) { + return false; + } + + if (!$this->dataStoreReader->isPrivateFeed($feedId)) { + return true; + } + + if ($accessToken === null) { + return false; + } + + $userId = (string) $accessToken->getUserId(); + + if ($this->dataStoreReader->hasFeedReadAccess($feedId, $userId)) { + return $this->checkScope($accessToken, 'feeds:read'); + } + + return false; + } + + public function hasFollowAccess(string $feedId, AccessToken $accessToken): bool + { + if (!$this->dataStoreReader->hasFeed($feedId)) { + return false; + } + + if (!$this->dataStoreReader->isPrivateFeed($feedId)) { + return true; + } + + $userId = (string) $accessToken->getUserId(); + + if ($this->dataStoreReader->hasFeedReadAccess($feedId, $userId)) { + return $this->checkScope($accessToken, 'feeds:read'); + } + + return false; + } + + public function hasWriteAccess(string $feedId, AccessToken $accessToken = null): bool + { + if ($accessToken === null) { + return false; + } + + if ($this->dataStoreReader->hasFeedWriteAccess($feedId, $accessToken->getUserId())) { + return $this->checkScope($accessToken, 'feeds:write'); + } + + return false; + } + + public function hasPostAccess(string $feedId, AccessToken $accessToken = null): bool + { + if ($accessToken === null) { + return false; + } + + $userId = (string) $accessToken->getUserId(); + + if ($this->dataStoreReader->hasFeedPostAccess($feedId, $userId)) { + return $this->checkScope($accessToken, 'feeds:post'); + } + + return false; + } + + public function canModifyFeedUser(string $feedId, string $userId, AccessToken $accessToken = null): bool + { + if ($accessToken === null) { + return false; + } + + if (!$this->hasWriteAccess($feedId, $accessToken)) { + return false; + } + + if ((string) $accessToken->getUserId() === $userId) { + return false; + } + + // Check if the target user is not an owner + if (!$this->dataStoreReader->hasFeedWriteAccess($feedId, $userId)) { + return true; + } + + // Last owner cannot be deleted + return $this->dataStoreReader->countFeedWriteAccess($feedId) > 1; + } + + public function canUnfollow(string $feedId, AccessToken $accessToken = null): bool + { + if ($accessToken === null) { + return false; + } + + // Check if the target user is not an owner + if (!$this->dataStoreReader->hasFeedWriteAccess($feedId, $accessToken->getUserId())) { + return true; + } + + // Last owner cannot unfollow + return $this->dataStoreReader->countFeedWriteAccess($feedId) > 1; + } + } +} diff --git a/API/src/Access/AccessTypes/AccessTypeInterface.php b/API/src/Access/AccessTypes/AccessTypeInterface.php new file mode 100644 index 0000000..188cc2f --- /dev/null +++ b/API/src/Access/AccessTypes/AccessTypeInterface.php @@ -0,0 +1,11 @@ + 1, + 'user:write' => 1, + 'collections:read' => 1, + 'collections:write' => 1, + 'public' => 1, + 'feeds:read' => 1, + 'feeds:write' => 1, + 'feeds:post' => 1 + ]; + + public function __construct(array $scopes) + { + $this->validate($scopes); + + $this->scopes = array_flip($scopes); + } + + private function validate(array $scopes) + { + foreach ($scopes as $scope) { + if (!isset($this->allowedScopes[$scope])) { + throw new \Exception('invalid scope \'' . $scope . '\''); + } + } + } + + public function hasScope(string $scope): bool + { + return isset($this->scopes[$scope]); + } + + public function jsonSerialize(): array + { + return array_keys($this->scopes); + } + } +} diff --git a/API/src/Access/AccessTypes/SystemAccess.php b/API/src/Access/AccessTypes/SystemAccess.php new file mode 100644 index 0000000..995237c --- /dev/null +++ b/API/src/Access/AccessTypes/SystemAccess.php @@ -0,0 +1,11 @@ +elasticBackend = $elasticBackend; + } + + public function search(string $query, SearchType $type, string $userId, int $limit, int $page): array + { + return $this->elasticBackend->search($type->getElasticType(), $limit * ($page - 1), $limit, [ + 'query' => [ + 'bool' => [ + 'should' => [ + [ + 'multi_match' => [ + 'query' => $query, + 'analyzer' => 'ttio_text', + 'type' => 'most_fields', + 'boost' => 2, + 'fields' => ['title', 'title.ngram', 'description', 'description.ngram'] + ] + ], + [ + 'multi_match' => [ + 'query' => $query, + 'boost' => 4, + 'type' => 'most_fields', + 'analyzer' => 'ttio_text', + 'fields' => ['name', 'name.ngram'] + ] + ], + [ + 'multi_match' => [ + 'query' => $query, + 'type' => 'most_fields', + 'analyzer' => 'ttio_text', + 'fields' => ['body', 'body.ngram'] + ], + ], + ], + 'minimum_should_match' => 1, + 'filter' => [ + 'terms' => [ + '_feed_id' => [ + 'index' => 'ttio', + 'type' => 'user', + 'id' => $userId, + 'path' => 'feeds' + ] + ] + ] + ] + ] + ]); + } + + public function getFeedPosts(string $feedId, int $limit, int $page): array + { + return $this->elasticBackend->search('post', $limit * ($page - 1), $limit, [ + 'sort' => [ + 'created' => 'desc' + ], + 'query' => [ + 'term' => [ + '_feed_id' => $feedId + ] + ] + ]); + } + + public function getUserFeeds(string $userId, int $limit, int $page): array + { + return $this->elasticBackend->search('feed', $limit * ($page - 1), $limit, [ + 'sort' => [ + 'created' => 'desc' + ], + 'query' => [ + 'terms' => [ + '_feed_id' => [ + 'index' => 'ttio', + 'type' => 'user', + 'id' => $userId, + 'path' => 'feeds' + ] + ] + ] + ]); + } + + public function getUserFeed(string $userId, int $limit, int $page): array + { + return $this->elasticBackend->search('post', $limit * ($page - 1), $limit, [ + 'sort' => [ + 'created' => 'desc' + ], + 'query' => [ + 'terms' => [ + '_feed_id' => [ + 'index' => 'ttio', + 'type' => 'user', + 'id' => $userId, + 'path' => 'feeds' + ] + ] + ] + ]); + } + } +} diff --git a/API/src/Bootstrap/Bootstrapper.php b/API/src/Bootstrap/Bootstrapper.php new file mode 100644 index 0000000..58bf02e --- /dev/null +++ b/API/src/Bootstrap/Bootstrapper.php @@ -0,0 +1,73 @@ +getConfiguration()); + + $factory->registerFactory(new \Timetabio\Framework\Factories\FrameworkFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\BackendFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\LoggerFactory); + + $factory->registerFactory(new \Timetabio\Library\Factories\LocatorFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\ServiceFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\MapperFactory); + + $factory->registerFactory(new \Timetabio\API\Factories\ControllerFactory); + $factory->registerFactory(new \Timetabio\API\Factories\ErrorHandlerFactory); + $factory->registerFactory(new \Timetabio\API\Factories\HandlerFactory); + $factory->registerFactory(new \Timetabio\API\Factories\QueryFactory); + $factory->registerFactory(new \Timetabio\API\Factories\RouterFactory); + $factory->registerFactory(new \Timetabio\API\Factories\EndpointFactory); + $factory->registerFactory(new \Timetabio\API\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\API\Factories\CommandFactory); + $factory->registerFactory(new \Timetabio\API\Factories\MapperFactory); + $factory->registerFactory(new \Timetabio\API\Factories\ServiceFactory); + $factory->registerFactory(new \Timetabio\API\Factories\BackendFactory); + + return $factory; + } + + protected function buildRouter(): RouterInterface + { + $router = new Router; + + $router->addRouter($this->getFactory()->createEndpointRouter()); + + return $router; + } + + protected function buildErrorHandler(): AbstractErrorHandler + { + if ($this->getConfiguration()->isDevelopmentMode()) { + return $this->getFactory()->createDevelopmentErrorHandler(); + } + + return $this->getFactory()->createProductionErrorHandler(); + } + } +} diff --git a/API/src/Builders/UriBuilder.php b/API/src/Builders/UriBuilder.php new file mode 100644 index 0000000..dda818a --- /dev/null +++ b/API/src/Builders/UriBuilder.php @@ -0,0 +1,24 @@ +s3UriBuilder = $s3UriBuilder; + } + + public function buildFileUri(string $publicId, string $filename): string + { + return $this->s3UriBuilder->buildObjectUrl($publicId . '/' . urlencode($filename)); + } + } +} diff --git a/API/src/Commands/BetaRequest/CreateBetaRequestCommand.php b/API/src/Commands/BetaRequest/CreateBetaRequestCommand.php new file mode 100644 index 0000000..ff70abf --- /dev/null +++ b/API/src/Commands/BetaRequest/CreateBetaRequestCommand.php @@ -0,0 +1,27 @@ +betaRequestService = $betaRequestService; + } + + public function execute(EmailAddress $email): array + { + return $this->betaRequestService->createBetaRequest($email); + } + } +} diff --git a/API/src/Commands/CreateCollectionCommand.php b/API/src/Commands/CreateCollectionCommand.php new file mode 100644 index 0000000..c96cac1 --- /dev/null +++ b/API/src/Commands/CreateCollectionCommand.php @@ -0,0 +1,28 @@ +collectionService = $collectionService; + } + + public function execute(CollectionName $collectionName, UserId $userId): array + { + return $this->collectionService->createCollection($collectionName, $userId); + } + } +} diff --git a/API/src/Commands/CreateFeedCommand.php b/API/src/Commands/CreateFeedCommand.php new file mode 100644 index 0000000..e76c20a --- /dev/null +++ b/API/src/Commands/CreateFeedCommand.php @@ -0,0 +1,47 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(UserId $owner, string $name, string $description, bool $isPrivate): array + { + $feed = $this->feedService->createFeed($owner, $name, $description, $isPrivate); + $feedId = $feed['id']; + + $this->dataStoreWriter->setFeedAccess($feedId, $owner, new \Timetabio\Library\UserRoles\Owner); + + if ($isPrivate) { + $this->dataStoreWriter->addPrivateFeed($feedId); + } + + $this->dataStoreWriter->addFeed($feedId); + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexFeedTask($feedId)); + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexUserTask($owner)); + + return $feed; + } + } +} diff --git a/API/src/Commands/DeleteAccessTokenCommand.php b/API/src/Commands/DeleteAccessTokenCommand.php new file mode 100644 index 0000000..7fb9a4f --- /dev/null +++ b/API/src/Commands/DeleteAccessTokenCommand.php @@ -0,0 +1,27 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function execute(AccessToken $accessToken) + { + $this->dataStoreWriter->removeAccessToken($accessToken); + } + } +} diff --git a/API/src/Commands/DeleteCollectionCommand.php b/API/src/Commands/DeleteCollectionCommand.php new file mode 100644 index 0000000..ac28871 --- /dev/null +++ b/API/src/Commands/DeleteCollectionCommand.php @@ -0,0 +1,27 @@ +collectionService = $collectionService; + } + + public function execute(CollectionId $collectionId) + { + $this->collectionService->deleteCollection($collectionId); + } + } +} diff --git a/API/src/Commands/Feed/CreateFeedUploadUrlCommand.php b/API/src/Commands/Feed/CreateFeedUploadUrlCommand.php new file mode 100644 index 0000000..5f745d7 --- /dev/null +++ b/API/src/Commands/Feed/CreateFeedUploadUrlCommand.php @@ -0,0 +1,36 @@ +awsS3Backend = $awsS3Backend; + } + + public function execute(string $filename, string $mimeType): UploadParams + { + $filename = new FeedFile($filename); + $file = new FileUpload($filename, $mimeType); + + return new UploadParams( + $filename, + $this->awsS3Backend->getEndpoint(), + $this->awsS3Backend->createUploadParams($file) + ); + } + } +} diff --git a/API/src/Commands/Feed/CreateInvitationCommand.php b/API/src/Commands/Feed/CreateInvitationCommand.php new file mode 100644 index 0000000..93804ee --- /dev/null +++ b/API/src/Commands/Feed/CreateInvitationCommand.php @@ -0,0 +1,38 @@ +invitationService = $invitationService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(FeedInvitation $invitation): array + { + $result = $this->invitationService->createInvitation($invitation); + + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\SendFeedInvitationTask($invitation)); + + return $result; + } + } +} diff --git a/API/src/Commands/Feed/DeleteInvitationCommand.php b/API/src/Commands/Feed/DeleteInvitationCommand.php new file mode 100644 index 0000000..f4ea7d1 --- /dev/null +++ b/API/src/Commands/Feed/DeleteInvitationCommand.php @@ -0,0 +1,26 @@ +invitationService = $invitationService; + } + + public function execute(string $feedId, string $userId) + { + $this->invitationService->deleteInvitation($feedId, $userId); + } + } +} diff --git a/API/src/Commands/Feed/SetFeedVanityCommand.php b/API/src/Commands/Feed/SetFeedVanityCommand.php new file mode 100644 index 0000000..44f81b8 --- /dev/null +++ b/API/src/Commands/Feed/SetFeedVanityCommand.php @@ -0,0 +1,41 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(string $feedId, string $vanity) + { + $this->dataStoreWriter->removeVanity($feedId); + + if ($vanity === '') { + $this->feedService->deleteFeedVanity($feedId); + return; + } + + $this->feedService->createFeedVanity($feedId, $vanity); + $this->dataStoreWriter->setVanity($feedId, $vanity); + } + } +} diff --git a/API/src/Commands/Feed/UpdateFeedUserCommand.php b/API/src/Commands/Feed/UpdateFeedUserCommand.php new file mode 100644 index 0000000..d062dd7 --- /dev/null +++ b/API/src/Commands/Feed/UpdateFeedUserCommand.php @@ -0,0 +1,37 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(string $feedId, string $userId, UserRole $role) + { + $this->feedService->updateFeedUser($feedId, $userId, $role); + + $this->dataStoreWriter->removeFeedAccess($feedId, $userId); + $this->dataStoreWriter->setFeedAccess($feedId, $userId, $role); + } + } +} diff --git a/API/src/Commands/Feed/UpdateInvitationCommand.php b/API/src/Commands/Feed/UpdateInvitationCommand.php new file mode 100644 index 0000000..45db581 --- /dev/null +++ b/API/src/Commands/Feed/UpdateInvitationCommand.php @@ -0,0 +1,27 @@ +invitationService = $invitationService; + } + + public function execute(string $feedId, string $userId, UserRole $role) + { + $this->invitationService->updateInvitation($feedId, $userId, $role); + } + } +} diff --git a/API/src/Commands/Feeds/CreateFeedPersonCommand.php b/API/src/Commands/Feeds/CreateFeedPersonCommand.php new file mode 100644 index 0000000..21402c7 --- /dev/null +++ b/API/src/Commands/Feeds/CreateFeedPersonCommand.php @@ -0,0 +1,38 @@ +peopleService = $peopleService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(string $feedId, string $userId, bool $post) + { + $this->peopleService->createPerson($feedId, $userId, $post); + $this->dataStoreWriter->addFeedReadAccess($feedId, $userId); + + if ($post) { + $this->dataStoreWriter->addFeedPostAccess($feedId, $userId); + } + } + } +} diff --git a/API/src/Commands/Feeds/DeleteFeedPersonCommand.php b/API/src/Commands/Feeds/DeleteFeedPersonCommand.php new file mode 100644 index 0000000..3223962 --- /dev/null +++ b/API/src/Commands/Feeds/DeleteFeedPersonCommand.php @@ -0,0 +1,37 @@ +peopleService = $peopleService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(string $feedId, string $userId) + { + $this->peopleService->deletePerson($feedId, $userId); + + $this->dataStoreWriter->removeFeedAccess($feedId, $userId); + + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexUserTask($userId)); + } + } +} diff --git a/API/src/Commands/Feeds/FollowFeedCommand.php b/API/src/Commands/Feeds/FollowFeedCommand.php new file mode 100644 index 0000000..4f41e2a --- /dev/null +++ b/API/src/Commands/Feeds/FollowFeedCommand.php @@ -0,0 +1,40 @@ +followerService = $followerService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(FeedId $feedId, UserId $userId, UserRole $role) + { + $this->followerService->followFeed($feedId, $userId, $role); + $this->dataStoreWriter->setFeedAccess($feedId, $userId, $role); + + $this->dataStoreWriter->queueTask(new IndexUserTask($userId)); + } + } +} diff --git a/API/src/Commands/Feeds/UnfollowFeedCommand.php b/API/src/Commands/Feeds/UnfollowFeedCommand.php new file mode 100644 index 0000000..6c6f8b7 --- /dev/null +++ b/API/src/Commands/Feeds/UnfollowFeedCommand.php @@ -0,0 +1,38 @@ +followerService = $followerService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(FeedId $feedId, UserId $userId) + { + $this->followerService->unfollowFeed($feedId, $userId); + $this->dataStoreWriter->removeFeedAccess($feedId, $userId); + $this->dataStoreWriter->queueTask(new IndexUserTask($userId)); + } + } +} diff --git a/API/src/Commands/Feeds/UpdateFeedCommand.php b/API/src/Commands/Feeds/UpdateFeedCommand.php new file mode 100644 index 0000000..beba9b3 --- /dev/null +++ b/API/src/Commands/Feeds/UpdateFeedCommand.php @@ -0,0 +1,35 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(FeedId $feedId, array $updates) + { + $this->feedService->updateFeed($feedId, $updates); + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexFeedTask($feedId)); + } + } +} diff --git a/API/src/Commands/File/CreateFileCommand.php b/API/src/Commands/File/CreateFileCommand.php new file mode 100644 index 0000000..d5bf579 --- /dev/null +++ b/API/src/Commands/File/CreateFileCommand.php @@ -0,0 +1,27 @@ +fileService = $fileService; + } + + public function execute(string $ownerId, FeedFile $file, string $mimeType): array + { + return $this->fileService->createFile($ownerId, $file->getPublicId(), $file->getFilename(), $mimeType); + } + } +} diff --git a/API/src/Commands/Posts/CreatePostCommand.php b/API/src/Commands/Posts/CreatePostCommand.php new file mode 100644 index 0000000..3108ee4 --- /dev/null +++ b/API/src/Commands/Posts/CreatePostCommand.php @@ -0,0 +1,70 @@ +inkBackend = $inkBackend; + $this->postService = $postService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute( + PostTypeInterface $type, + string $title, + string $body, + string $feedId, + string $authorId, + Timestamp $timestamp = null, + array $attachments + ): array + { + $inkResult = $this->inkBackend->process($body); + $post = $this->postService->createPost($type, $feedId, $authorId, $title, $body, $timestamp); + $postId = $post['id']; + + /** @var Attachment $attachment */ + foreach ($attachments as $attachment) { + $this->postService->createAttachment($postId, $attachment); + } + + $this->dataStoreWriter->setPostBody($postId, $inkResult->getBody()); + $this->dataStoreWriter->setPostPreview($postId, $inkResult->getPreview()); + $this->dataStoreWriter->setPostText($postId, $inkResult->getPlainText()); + + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexPostTask($postId)); + + return $post; + } + } +} diff --git a/API/src/Commands/Posts/DeletePostCommand.php b/API/src/Commands/Posts/DeletePostCommand.php new file mode 100644 index 0000000..0ecf820 --- /dev/null +++ b/API/src/Commands/Posts/DeletePostCommand.php @@ -0,0 +1,39 @@ +postService = $postService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function execute(string $postId) + { + $this->postService->deletePost($postId); + + $this->dataStoreWriter->removePostBody($postId); + $this->dataStoreWriter->removePostPreview($postId); + $this->dataStoreWriter->removePostText($postId); + + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexPostTask($postId)); + } + } +} diff --git a/API/src/Commands/SaveAccessTokenCommand.php b/API/src/Commands/SaveAccessTokenCommand.php new file mode 100644 index 0000000..aa9b4da --- /dev/null +++ b/API/src/Commands/SaveAccessTokenCommand.php @@ -0,0 +1,27 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function execute(AccessToken $accessToken) + { + $this->dataStoreWriter->saveAccessToken($accessToken); + } + } +} diff --git a/API/src/Commands/SendVerificationCommand.php b/API/src/Commands/SendVerificationCommand.php new file mode 100644 index 0000000..31580b9 --- /dev/null +++ b/API/src/Commands/SendVerificationCommand.php @@ -0,0 +1,29 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function execute(EmailPerson $person, Token $token) + { + $this->dataStoreWriter->queueTask(new SendVerificationEmailTask($person, $token)); + } + } +} diff --git a/API/src/Commands/UpdateCollectionCommand.php b/API/src/Commands/UpdateCollectionCommand.php new file mode 100644 index 0000000..584bd23 --- /dev/null +++ b/API/src/Commands/UpdateCollectionCommand.php @@ -0,0 +1,28 @@ +collectionService = $collectionService; + } + + public function execute(CollectionId $collectionId, array $updates) + { + $this->collectionService->updateCollection($collectionId, $updates); + } + } +} diff --git a/API/src/Commands/User/CreateUserCommand.php b/API/src/Commands/User/CreateUserCommand.php new file mode 100644 index 0000000..63a5f60 --- /dev/null +++ b/API/src/Commands/User/CreateUserCommand.php @@ -0,0 +1,30 @@ +userService = $userService; + } + + public function execute(EmailAddress $email, Username $username, Password $password, Token $token): array + { + return $this->userService->createUser($email, $username, $password, $token); + } + } +} diff --git a/API/src/Commands/User/UpdateUserCommand.php b/API/src/Commands/User/UpdateUserCommand.php new file mode 100644 index 0000000..e93d769 --- /dev/null +++ b/API/src/Commands/User/UpdateUserCommand.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(UserId $userId, array $data) + { + $this->userService->updateUser($userId, $data); + } + } +} diff --git a/API/src/Commands/User/VerifyUserCommand.php b/API/src/Commands/User/VerifyUserCommand.php new file mode 100644 index 0000000..589bbd8 --- /dev/null +++ b/API/src/Commands/User/VerifyUserCommand.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(UserId $userId) + { + $this->userService->verifyUser($userId); + } + } +} diff --git a/API/src/DataStore/DataStoreReader.php b/API/src/DataStore/DataStoreReader.php new file mode 100644 index 0000000..715bd3d --- /dev/null +++ b/API/src/DataStore/DataStoreReader.php @@ -0,0 +1,38 @@ +getDataStore()->has('access_token_' . $token); + } + + public function getAccessToken(string $token): AccessToken + { + return unserialize($this->getDataStore()->get('access_token_' . $token)); + } + + /** + * @deprecated + */ + public function isFeedPrivate(string $feedId): bool + { + return $this->getDataStore()->has('feed_access_is_private_' . $feedId); + } + + /** + * @deprecated + */ + public function isFeedOwner(string $feedId, string $userId): bool + { + return $this->getDataStore()->get('feed_access_owner_' . $feedId) === $userId; + } + } +} diff --git a/API/src/DataStore/DataStoreWriter.php b/API/src/DataStore/DataStoreWriter.php new file mode 100644 index 0000000..70370e2 --- /dev/null +++ b/API/src/DataStore/DataStoreWriter.php @@ -0,0 +1,147 @@ +getToken(); + + $this->getDataStore()->set( + $key, + serialize($token) + ); + + $this->getDataStore()->setTimeout($key, $token->getExpires()); + } + + public function renewAccessToken(AccessToken $token) + { + $this->getDataStore()->setTimeout('access_token_' . $token->getToken(), $token->getExpires()); + } + + public function removeAccessToken(AccessToken $token) + { + $this->getDataStore()->remove('access_token_' . $token->getToken()); + } + + /** + * @deprecated + */ + public function setFeedOwner(string $feedId, string $userId) + { + $this->getDataStore()->set('feed_access_owner_' . $feedId, $userId); + } + + /** + * @deprecated + */ + public function setPrivateFeed(string $feedId) + { + $this->getDataStore()->set('feed_access_is_private_' . $feedId, 1); + } + + /** + * @deprecated + */ + public function addFeedReadAccess(string $feedId, string $userId) + { + $this->getDataStore()->addToSet('feed_access_read_' . $feedId, $userId); + } + + /** + * @deprecated + */ + public function addFeedPostAccess(string $feedId, string $userId) + { + $this->getDataStore()->addToSet('feed_access_post_' . $feedId, $userId); + } + + /** + * @deprecated + */ + public function removeFeedReadAccess(string $feedId, string $userId) + { + $this->getDataStore()->removeFromSet('feed_access_read_' . $feedId, $userId); + } + + /** + * @deprecated + */ + public function removeFeedPostAccess(string $feedId, string $userId) + { + $this->getDataStore()->removeFromSet('feed_access_post_' . $feedId, $userId); + } + + /** + * @deprecated + */ + public function setPostBody(string $postId, string $body) + { + $this->getDataStore()->set('post_body:' . $postId, $body); + } + + /** + * @deprecated + */ + public function setPostPreview(string $postId, string $body) + { + $this->getDataStore()->set('post_preview:' . $postId, $body); + } + + /** + * @deprecated + */ + public function setPostText(string $postId, string $body) + { + $this->getDataStore()->set('post_text:' . $postId, $body); + } + + /** + * @deprecated + */ + public function removePostBody(string $postId) + { + $this->getDataStore()->remove('post_body:' . $postId); + } + + /** + * @deprecated + */ + public function removePostPreview(string $postId) + { + $this->getDataStore()->remove('post_preview:' . $postId); + } + + /** + * @deprecated + */ + public function removePostText(string $postId) + { + $this->getDataStore()->remove('post_text:' . $postId); + } + + /** + * @deprecated + */ + public function removeVanity(string $feedId) + { + $key = 'feed_vanity:' . $feedId; + + if (!$this->getDataStore()->has($key)) { + return; + } + + $vanity = $this->getDataStore()->get($key); + + $this->getDataStore()->remove($key); + $this->getDataStore()->remove('vanity_feed:' . mb_strtolower($vanity)); + } + } +} diff --git a/API/src/Endpoints/AbstractEndpoint.php b/API/src/Endpoints/AbstractEndpoint.php new file mode 100644 index 0000000..f9256f9 --- /dev/null +++ b/API/src/Endpoints/AbstractEndpoint.php @@ -0,0 +1,71 @@ +factory = $factory; + } + + public function canHandle(RequestInterface $request): bool + { + return $this->validate($request->getUri()); + } + + public function handle(RequestInterface $request): ControllerInterface + { + if (get_class($request) !== $this->getRequestType()) { + throw new \Exception; + } + + return $this->doHandle($request); + } + + protected function validate(Uri $uri): bool + { + if (strpos($this->getEndpoint(), ':') === false) { + return $this->getEndpoint() === $uri->getPath(); + } + + $explodedPath = $uri->getExplodedPath(); + $explodedEndpointPath = explode('/', ltrim($this->getEndpoint(), '/')); + + if (count($explodedPath) !== count($explodedEndpointPath)) { + return false; + } + + foreach ($explodedEndpointPath as $i => $part) { + if (strpos($part, ':') !== false) { + continue; + } + + if ($part !== $explodedPath[$i]) { + return false; + } + } + + return true; + } + + protected function getFactory(): MasterFactoryInterface + { + return $this->factory; + } + + abstract protected function doHandle(RequestInterface $request): ControllerInterface; + } +} diff --git a/API/src/Endpoints/AuthEndpoint.php b/API/src/Endpoints/AuthEndpoint.php new file mode 100644 index 0000000..adbc7d1 --- /dev/null +++ b/API/src/Endpoints/AuthEndpoint.php @@ -0,0 +1,33 @@ +getFactory()->createAuthController(); + } + } +} diff --git a/API/src/Endpoints/BetaRequest/CreateBetaRequestEndpoint.php b/API/src/Endpoints/BetaRequest/CreateBetaRequestEndpoint.php new file mode 100644 index 0000000..263b714 --- /dev/null +++ b/API/src/Endpoints/BetaRequest/CreateBetaRequestEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createCreateBetaRequestController(); + } + + public function getEndpoint(): string + { + return '/v1/beta_requests'; + } + + public function getRequestType(): string + { + return \Timetabio\Framework\Http\Request\PostRequest::class; + } + + public function hasAccess(AccessTypeInterface $accessType): bool + { + return $accessType instanceof \Timetabio\API\Access\AccessTypes\SystemAccess; + } + } +} diff --git a/API/src/Endpoints/Collections/CreateCollectionEndpoint.php b/API/src/Endpoints/Collections/CreateCollectionEndpoint.php new file mode 100644 index 0000000..5bb949b --- /dev/null +++ b/API/src/Endpoints/Collections/CreateCollectionEndpoint.php @@ -0,0 +1,44 @@ +hasScope('collections:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createCreateCollectionController(); + } + } +} diff --git a/API/src/Endpoints/Collections/DeleteCollectionEndpoint.php b/API/src/Endpoints/Collections/DeleteCollectionEndpoint.php new file mode 100644 index 0000000..8a4d97e --- /dev/null +++ b/API/src/Endpoints/Collections/DeleteCollectionEndpoint.php @@ -0,0 +1,44 @@ +hasScope('collections:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createDeleteCollectionController(); + } + } +} diff --git a/API/src/Endpoints/Collections/GetCollectionEndpoint.php b/API/src/Endpoints/Collections/GetCollectionEndpoint.php new file mode 100644 index 0000000..bf2506c --- /dev/null +++ b/API/src/Endpoints/Collections/GetCollectionEndpoint.php @@ -0,0 +1,44 @@ +hasScope('collections:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetCollectionController(); + } + } +} diff --git a/API/src/Endpoints/Collections/UpdateCollectionEndpoint.php b/API/src/Endpoints/Collections/UpdateCollectionEndpoint.php new file mode 100644 index 0000000..266c21f --- /dev/null +++ b/API/src/Endpoints/Collections/UpdateCollectionEndpoint.php @@ -0,0 +1,44 @@ +hasScope('collections:write'); //todo: write or create for creating new collection? + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateCollectionController(); + } + } +} diff --git a/API/src/Endpoints/EndpointInterface.php b/API/src/Endpoints/EndpointInterface.php new file mode 100644 index 0000000..cd76b91 --- /dev/null +++ b/API/src/Endpoints/EndpointInterface.php @@ -0,0 +1,23 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createCreateFeedController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/CreateInvitationEndpoint.php b/API/src/Endpoints/Feeds/CreateInvitationEndpoint.php new file mode 100644 index 0000000..9ae609b --- /dev/null +++ b/API/src/Endpoints/Feeds/CreateInvitationEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createCreateFeedInvitationController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/CreateUploadEndpoint.php b/API/src/Endpoints/Feeds/CreateUploadEndpoint.php new file mode 100644 index 0000000..394cb11 --- /dev/null +++ b/API/src/Endpoints/Feeds/CreateUploadEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createCreateFeedUploadUrlController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/DeleteFeedInvitationEndpoint.php b/API/src/Endpoints/Feeds/DeleteFeedInvitationEndpoint.php new file mode 100644 index 0000000..1c0abf3 --- /dev/null +++ b/API/src/Endpoints/Feeds/DeleteFeedInvitationEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createDeleteFeedInvitationController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/DeleteFeedUserEndpoint.php b/API/src/Endpoints/Feeds/DeleteFeedUserEndpoint.php new file mode 100644 index 0000000..e6a1599 --- /dev/null +++ b/API/src/Endpoints/Feeds/DeleteFeedUserEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createDeleteFeedPersonController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/FollowFeedEndpoint.php b/API/src/Endpoints/Feeds/FollowFeedEndpoint.php new file mode 100644 index 0000000..ba7ea67 --- /dev/null +++ b/API/src/Endpoints/Feeds/FollowFeedEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:follow'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createFollowFeedController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/GetFeedEndpoint.php b/API/src/Endpoints/Feeds/GetFeedEndpoint.php new file mode 100644 index 0000000..842a987 --- /dev/null +++ b/API/src/Endpoints/Feeds/GetFeedEndpoint.php @@ -0,0 +1,35 @@ +getFactory()->createGetFeedController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/GetFeedInvitationsEndpoint.php b/API/src/Endpoints/Feeds/GetFeedInvitationsEndpoint.php new file mode 100644 index 0000000..ce0b9bb --- /dev/null +++ b/API/src/Endpoints/Feeds/GetFeedInvitationsEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetFeedInvitationsController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/GetFeedUsersEndpoint.php b/API/src/Endpoints/Feeds/GetFeedUsersEndpoint.php new file mode 100644 index 0000000..a95a673 --- /dev/null +++ b/API/src/Endpoints/Feeds/GetFeedUsersEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createGetFeedUsersController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/GetFeedsEndpoint.php b/API/src/Endpoints/Feeds/GetFeedsEndpoint.php new file mode 100644 index 0000000..6e289e0 --- /dev/null +++ b/API/src/Endpoints/Feeds/GetFeedsEndpoint.php @@ -0,0 +1,45 @@ +hasScope('public'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetFeedsController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/UnfollowFeedEndpoint.php b/API/src/Endpoints/Feeds/UnfollowFeedEndpoint.php new file mode 100644 index 0000000..e9c2e94 --- /dev/null +++ b/API/src/Endpoints/Feeds/UnfollowFeedEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:follow'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUnfollowFeedController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/UpdateFeedEndpoint.php b/API/src/Endpoints/Feeds/UpdateFeedEndpoint.php new file mode 100644 index 0000000..b624e21 --- /dev/null +++ b/API/src/Endpoints/Feeds/UpdateFeedEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateFeedController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/UpdateFeedInvitationEndpoint.php b/API/src/Endpoints/Feeds/UpdateFeedInvitationEndpoint.php new file mode 100644 index 0000000..e20494c --- /dev/null +++ b/API/src/Endpoints/Feeds/UpdateFeedInvitationEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateFeedInvitationController(); + } + } +} diff --git a/API/src/Endpoints/Feeds/UpdateFeedUserEndpoint.php b/API/src/Endpoints/Feeds/UpdateFeedUserEndpoint.php new file mode 100644 index 0000000..47916e6 --- /dev/null +++ b/API/src/Endpoints/Feeds/UpdateFeedUserEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateFeedUserController(); + } + } +} diff --git a/API/src/Endpoints/Index/GetIndexEndpoint.php b/API/src/Endpoints/Index/GetIndexEndpoint.php new file mode 100644 index 0000000..0e4703e --- /dev/null +++ b/API/src/Endpoints/Index/GetIndexEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createGetIndexController(); + } + } +} diff --git a/API/src/Endpoints/Posts/CreatePostEndpoint.php b/API/src/Endpoints/Posts/CreatePostEndpoint.php new file mode 100644 index 0000000..e141ea3 --- /dev/null +++ b/API/src/Endpoints/Posts/CreatePostEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:post'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createCreatePostController(); + } + } +} diff --git a/API/src/Endpoints/Posts/DeletePostEndpoint.php b/API/src/Endpoints/Posts/DeletePostEndpoint.php new file mode 100644 index 0000000..0e85adb --- /dev/null +++ b/API/src/Endpoints/Posts/DeletePostEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:post'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createDeletePostController(); + } + } +} diff --git a/API/src/Endpoints/Posts/GetPostEndpoint.php b/API/src/Endpoints/Posts/GetPostEndpoint.php new file mode 100644 index 0000000..e7a93c7 --- /dev/null +++ b/API/src/Endpoints/Posts/GetPostEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createGetPostController(); + } + } +} diff --git a/API/src/Endpoints/Posts/GetPostsEndpoint.php b/API/src/Endpoints/Posts/GetPostsEndpoint.php new file mode 100644 index 0000000..d07ce2a --- /dev/null +++ b/API/src/Endpoints/Posts/GetPostsEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createGetFeedPostsController(); + } + } +} diff --git a/API/src/Endpoints/Posts/UpdatePostEndpoint.php b/API/src/Endpoints/Posts/UpdatePostEndpoint.php new file mode 100644 index 0000000..730a6ba --- /dev/null +++ b/API/src/Endpoints/Posts/UpdatePostEndpoint.php @@ -0,0 +1,44 @@ +hasScope('feeds:post'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdatePostController(); + } + } +} diff --git a/API/src/Endpoints/Profiles/GetProfileEndpoint.php b/API/src/Endpoints/Profiles/GetProfileEndpoint.php new file mode 100644 index 0000000..4ae7bbc --- /dev/null +++ b/API/src/Endpoints/Profiles/GetProfileEndpoint.php @@ -0,0 +1,49 @@ +hasScope('public'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetProfileController(); + } + } +} diff --git a/API/src/Endpoints/Random/GetRandomEndpoint.php b/API/src/Endpoints/Random/GetRandomEndpoint.php new file mode 100644 index 0000000..4b71cc8 --- /dev/null +++ b/API/src/Endpoints/Random/GetRandomEndpoint.php @@ -0,0 +1,34 @@ +getFactory()->createGetRandomController(); + } + } +} diff --git a/API/src/Endpoints/RevokeEndpoint.php b/API/src/Endpoints/RevokeEndpoint.php new file mode 100644 index 0000000..303261e --- /dev/null +++ b/API/src/Endpoints/RevokeEndpoint.php @@ -0,0 +1,33 @@ +getFactory()->createRevokeController(); + } + } +} diff --git a/API/src/Endpoints/SearchEndpoint.php b/API/src/Endpoints/SearchEndpoint.php new file mode 100644 index 0000000..570d01d --- /dev/null +++ b/API/src/Endpoints/SearchEndpoint.php @@ -0,0 +1,43 @@ +hasScope('feeds:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createSearchController(); + } + } +} diff --git a/API/src/Endpoints/User/GetTodoEndpoint.php b/API/src/Endpoints/User/GetTodoEndpoint.php new file mode 100644 index 0000000..a9e4531 --- /dev/null +++ b/API/src/Endpoints/User/GetTodoEndpoint.php @@ -0,0 +1,44 @@ +getFactory()->createGetUserTodoController(); + } + + public function getEndpoint(): string + { + return '/v1/user/todo'; + } + + public function getRequestType(): string + { + return \Timetabio\Framework\Http\Request\GetRequest::class; + } + + public function hasAccess(AccessTypeInterface $accessType): bool + { + if ($accessType instanceof FullAccess) { + return true; + } + + if ($accessType instanceof ScopedAccess) { + return $accessType->hasScope('feeds:read'); + } + + return false; + } + } +} diff --git a/API/src/Endpoints/User/GetUpcomingEndpoint.php b/API/src/Endpoints/User/GetUpcomingEndpoint.php new file mode 100644 index 0000000..9ff5db2 --- /dev/null +++ b/API/src/Endpoints/User/GetUpcomingEndpoint.php @@ -0,0 +1,44 @@ +getFactory()->createGetUserUpcomingController(); + } + + public function getEndpoint(): string + { + return '/v1/user/upcoming'; + } + + public function getRequestType(): string + { + return \Timetabio\Framework\Http\Request\GetRequest::class; + } + + public function hasAccess(AccessTypeInterface $accessType): bool + { + if ($accessType instanceof FullAccess) { + return true; + } + + if ($accessType instanceof ScopedAccess) { + return $accessType->hasScope('feeds:read'); + } + + return false; + } + } +} diff --git a/API/src/Endpoints/User/GetUserCollectionsEndpoint.php b/API/src/Endpoints/User/GetUserCollectionsEndpoint.php new file mode 100644 index 0000000..0b7862f --- /dev/null +++ b/API/src/Endpoints/User/GetUserCollectionsEndpoint.php @@ -0,0 +1,44 @@ +hasScope('collections:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetUserCollectionsController(); + } + } +} diff --git a/API/src/Endpoints/User/GetUserEndpoint.php b/API/src/Endpoints/User/GetUserEndpoint.php new file mode 100644 index 0000000..af6bc69 --- /dev/null +++ b/API/src/Endpoints/User/GetUserEndpoint.php @@ -0,0 +1,44 @@ +hasScope('user:read'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createGetUserController(); + } + } +} diff --git a/API/src/Endpoints/User/GetUserFeedEndpoint.php b/API/src/Endpoints/User/GetUserFeedEndpoint.php new file mode 100644 index 0000000..cc200ef --- /dev/null +++ b/API/src/Endpoints/User/GetUserFeedEndpoint.php @@ -0,0 +1,44 @@ +getFactory()->createGetUserFeedController(); + } + + public function getEndpoint(): string + { + return '/v1/user/feed'; + } + + public function getRequestType(): string + { + return \Timetabio\Framework\Http\Request\GetRequest::class; + } + + public function hasAccess(AccessTypeInterface $accessType): bool + { + if ($accessType instanceof FullAccess) { + return true; + } + + if ($accessType instanceof ScopedAccess) { + return $accessType->hasScope('feeds:read'); + } + + return false; + } + } +} diff --git a/API/src/Endpoints/User/GetUserFeedsEndpoint.php b/API/src/Endpoints/User/GetUserFeedsEndpoint.php new file mode 100644 index 0000000..9acf24f --- /dev/null +++ b/API/src/Endpoints/User/GetUserFeedsEndpoint.php @@ -0,0 +1,44 @@ +getFactory()->createGetUserFeedsController(); + } + + public function getEndpoint(): string + { + return '/v1/user/feeds'; + } + + public function getRequestType(): string + { + return \Timetabio\Framework\Http\Request\GetRequest::class; + } + + public function hasAccess(AccessTypeInterface $accessType): bool + { + if ($accessType instanceof FullAccess) { + return true; + } + + if ($accessType instanceof ScopedAccess) { + return $accessType->hasScope('feeds:read'); + } + + return false; + } + } +} diff --git a/API/src/Endpoints/User/UpdateUserEndpoint.php b/API/src/Endpoints/User/UpdateUserEndpoint.php new file mode 100644 index 0000000..2f340f6 --- /dev/null +++ b/API/src/Endpoints/User/UpdateUserEndpoint.php @@ -0,0 +1,44 @@ +hasScope('user:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateUserController(); + } + } +} diff --git a/API/src/Endpoints/User/UpdateUserPasswordEndpoint.php b/API/src/Endpoints/User/UpdateUserPasswordEndpoint.php new file mode 100644 index 0000000..d2d490a --- /dev/null +++ b/API/src/Endpoints/User/UpdateUserPasswordEndpoint.php @@ -0,0 +1,41 @@ +hasScope('user:write'); + } + + return false; + } + + protected function doHandle(RequestInterface $request): ControllerInterface + { + return $this->getFactory()->createUpdateUserPasswordController(); + } + } +} diff --git a/API/src/Endpoints/Users/CreateUserEndpoint.php b/API/src/Endpoints/Users/CreateUserEndpoint.php new file mode 100644 index 0000000..a14e4ac --- /dev/null +++ b/API/src/Endpoints/Users/CreateUserEndpoint.php @@ -0,0 +1,35 @@ +getFactory()->createCreateUserController(); + } + } +} diff --git a/API/src/Endpoints/Verify/ResendEndpoint.php b/API/src/Endpoints/Verify/ResendEndpoint.php new file mode 100644 index 0000000..4790ce3 --- /dev/null +++ b/API/src/Endpoints/Verify/ResendEndpoint.php @@ -0,0 +1,35 @@ +getFactory()->createResendVerificationController(); + } + } +} diff --git a/API/src/Endpoints/Verify/VerifyEndpoint.php b/API/src/Endpoints/Verify/VerifyEndpoint.php new file mode 100644 index 0000000..e8aba9b --- /dev/null +++ b/API/src/Endpoints/Verify/VerifyEndpoint.php @@ -0,0 +1,35 @@ +getFactory()->createVerifyController(); + } + } +} diff --git a/API/src/ErrorHandlers/DevelopmentErrorHandler.php b/API/src/ErrorHandlers/DevelopmentErrorHandler.php new file mode 100644 index 0000000..d212f4a --- /dev/null +++ b/API/src/ErrorHandlers/DevelopmentErrorHandler.php @@ -0,0 +1,55 @@ +getStatusCode($exception)); + header('content-type: application/json'); + + echo json_encode([ + 'error' => $this->getError($exception), + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace() + ], JSON_PRETTY_PRINT); + + if (!($exception instanceof AbstractException)) { + $this->logger->error($exception); + } + + die(); + } + + public function getStatusCode(\Throwable $exception): int + { + if ($exception instanceof AbstractException) { + return $exception->getStatusCode()->getCode(); + } + + return 500; + } + + private function getError(\Throwable $exception): string + { + if ($exception instanceof AbstractException) { + return $exception->getId(); + } + + return 'internal_error'; + } + } +} diff --git a/API/src/ErrorHandlers/ProductionErrorHandler.php b/API/src/ErrorHandlers/ProductionErrorHandler.php new file mode 100644 index 0000000..071a716 --- /dev/null +++ b/API/src/ErrorHandlers/ProductionErrorHandler.php @@ -0,0 +1,67 @@ +getStatusCode($exception)); + header('content-type: application/json'); + + echo json_encode([ + 'error' => $this->getError($exception), + 'message' => $this->getMessage($exception), + ], JSON_PRETTY_PRINT); + + $this->logException($exception); + + die(); + } + + private function getError(\Throwable $exception): string + { + if ($exception instanceof AbstractException) { + return $exception->getId(); + } + + return 'internal_error'; + } + + private function getMessage(\Throwable $exception): string + { + if ($exception instanceof AbstractException) { + return $exception->getMessage(); + } + + return 'internal server error'; + } + + private function getStatusCode(\Throwable $exception): int + { + if ($exception instanceof AbstractException) { + return $exception->getStatusCode()->getCode(); + } + + return 500; + } + + private function logException(\Throwable $exception) + { + if ($exception instanceof AbstractException) { + return; + } + + $this->getLogger()->error($exception); + } + } +} diff --git a/API/src/Exceptions/AbstractException.php b/API/src/Exceptions/AbstractException.php new file mode 100644 index 0000000..52eb492 --- /dev/null +++ b/API/src/Exceptions/AbstractException.php @@ -0,0 +1,30 @@ +id = $id; + } + + public function getId(): string + { + return $this->id; + } + + abstract public function getStatusCode(): StatusCodeInterface; + } +} diff --git a/API/src/Exceptions/BadRequest.php b/API/src/Exceptions/BadRequest.php new file mode 100644 index 0000000..db6336e --- /dev/null +++ b/API/src/Exceptions/BadRequest.php @@ -0,0 +1,16 @@ +getMasterFactory()->createDataStoreReader(), + $this->getMasterFactory()->createRequestTokenReader() + ); + } + + public function createDataStoreReader(): \Timetabio\API\DataStore\DataStoreReader + { + return new \Timetabio\API\DataStore\DataStoreReader( + $this->getMasterFactory()->createRedisBackend() + ); + } + + public function createDataStoreWriter(): \Timetabio\API\DataStore\DataStoreWriter + { + return new \Timetabio\API\DataStore\DataStoreWriter( + $this->getMasterFactory()->createRedisBackend() + ); + } + + public function createRequestTokenReader(): \Timetabio\API\Readers\RequestTokenReader + { + return new \Timetabio\API\Readers\RequestTokenReader; + } + + public function createFeedAccessControl(): \Timetabio\API\Access\AccessControl\FeedAccessControl + { + return new \Timetabio\API\Access\AccessControl\FeedAccessControl( + $this->getMasterFactory()->createDataStoreReader() + ); + } + + public function createCollectionAccessControl(): \Timetabio\API\Access\AccessControl\CollectionAccessControl + { + return new \Timetabio\API\Access\AccessControl\CollectionAccessControl; + } + + public function createPostTypeLocator(): \Timetabio\API\Locators\PostTypeLocator + { + return new \Timetabio\API\Locators\PostTypeLocator; + } + + public function createUriBuilder(): \Timetabio\API\Builders\UriBuilder + { + return new \Timetabio\API\Builders\UriBuilder( + $this->getMasterFactory()->createS3HelperUriBuilder() + ); + } + + public function createSearchBackend(): \Timetabio\API\Backends\SearchBackend + { + return new \Timetabio\API\Backends\SearchBackend( + $this->getMasterFactory()->createElasticBackend() + ); + } + } +} diff --git a/API/src/Factories/BackendFactory.php b/API/src/Factories/BackendFactory.php new file mode 100644 index 0000000..6fb907a --- /dev/null +++ b/API/src/Factories/BackendFactory.php @@ -0,0 +1,13 @@ +getMasterFactory()->createUserService() + ); + } + + public function createVerifyUserCommand(): \Timetabio\API\Commands\User\VerifyUserCommand + { + return new \Timetabio\API\Commands\User\VerifyUserCommand( + $this->getMasterFactory()->createUserService() + ); + } + + public function createSaveAccessTokenCommand(): \Timetabio\API\Commands\SaveAccessTokenCommand + { + return new \Timetabio\API\Commands\SaveAccessTokenCommand( + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createSendVerificationCommand(): \Timetabio\API\Commands\SendVerificationCommand + { + return new \Timetabio\API\Commands\SendVerificationCommand( + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createUpdateUserCommand(): \Timetabio\API\Commands\User\UpdateUserCommand + { + return new \Timetabio\API\Commands\User\UpdateUserCommand( + $this->getMasterFactory()->createUserService() + ); + } + + public function createCreateCollectionCommand(): \Timetabio\API\Commands\CreateCollectionCommand + { + return new \Timetabio\API\Commands\CreateCollectionCommand( + $this->getMasterFactory()->createCollectionService() + ); + } + + public function createDeleteCollectionCommand(): \Timetabio\API\Commands\DeleteCollectionCommand + { + return new \Timetabio\API\Commands\DeleteCollectionCommand( + $this->getMasterFactory()->createCollectionService() + ); + } + + public function createUpdateCollectionCommand(): \Timetabio\API\Commands\UpdateCollectionCommand + { + return new \Timetabio\API\Commands\UpdateCollectionCommand( + $this->getMasterFactory()->createCollectionService() + ); + } + + public function createCreateFeedCommand(): \Timetabio\API\Commands\CreateFeedCommand + { + return new \Timetabio\API\Commands\CreateFeedCommand( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createUpdateFeedCommand(): \Timetabio\API\Commands\Feeds\UpdateFeedCommand + { + return new \Timetabio\API\Commands\Feeds\UpdateFeedCommand( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createFollowFeedCommand(): \Timetabio\API\Commands\Feeds\FollowFeedCommand + { + return new \Timetabio\API\Commands\Feeds\FollowFeedCommand( + $this->getMasterFactory()->createFollowerService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createUnfollowFeedCommand(): \Timetabio\API\Commands\Feeds\UnfollowFeedCommand + { + return new \Timetabio\API\Commands\Feeds\UnfollowFeedCommand( + $this->getMasterFactory()->createFollowerService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createCreateFeedPersonCommand(): \Timetabio\API\Commands\Feeds\CreateFeedPersonCommand + { + return new \Timetabio\API\Commands\Feeds\CreateFeedPersonCommand( + $this->getMasterFactory()->createPeopleService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createDeleteFeedPersonCommand(): \Timetabio\API\Commands\Feeds\DeleteFeedPersonCommand + { + return new \Timetabio\API\Commands\Feeds\DeleteFeedPersonCommand( + $this->getMasterFactory()->createPeopleService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createCreatePostCommand(): \Timetabio\API\Commands\Posts\CreatePostCommand + { + return new \Timetabio\API\Commands\Posts\CreatePostCommand( + $this->getMasterFactory()->createInkBackend(), + $this->getMasterFactory()->createPostService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createDeletePostCommand(): \Timetabio\API\Commands\Posts\DeletePostCommand + { + return new \Timetabio\API\Commands\Posts\DeletePostCommand( + $this->getMasterFactory()->createPostService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createSetFeedVanityCommand(): \Timetabio\API\Commands\Feed\SetFeedVanityCommand + { + return new \Timetabio\API\Commands\Feed\SetFeedVanityCommand( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createCreateFeedUploadUrlCommand(): \Timetabio\API\Commands\Feed\CreateFeedUploadUrlCommand + { + return new \Timetabio\API\Commands\Feed\CreateFeedUploadUrlCommand( + $this->getMasterFactory()->createAwsS3Backend() + ); + } + + public function createCreateFileCommand(): \Timetabio\API\Commands\File\CreateFileCommand + { + return new \Timetabio\API\Commands\File\CreateFileCommand( + $this->getMasterFactory()->createFileService() + ); + } + + public function createDeleteAccessTokenCommand(): \Timetabio\API\Commands\DeleteAccessTokenCommand + { + return new \Timetabio\API\Commands\DeleteAccessTokenCommand( + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createCreateBetaRequestCommand(): \Timetabio\API\Commands\BetaRequest\CreateBetaRequestCommand + { + return new \Timetabio\API\Commands\BetaRequest\CreateBetaRequestCommand( + $this->getMasterFactory()->createBetaRequestService() + ); + } + + public function createCreateInvitationCommand(): \Timetabio\API\Commands\Feed\CreateInvitationCommand + { + return new \Timetabio\API\Commands\Feed\CreateInvitationCommand( + $this->getMasterFactory()->createFeedInvitationService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createDeleteInvitationCommand(): \Timetabio\API\Commands\Feed\DeleteInvitationCommand + { + return new \Timetabio\API\Commands\Feed\DeleteInvitationCommand( + $this->getMasterFactory()->createFeedInvitationService() + ); + } + + public function createUpdateInvitationCommand(): \Timetabio\API\Commands\Feed\UpdateInvitationCommand + { + return new \Timetabio\API\Commands\Feed\UpdateInvitationCommand( + $this->getMasterFactory()->createFeedInvitationService() + ); + } + + public function createUpdateFeedUserCommand(): \Timetabio\API\Commands\Feed\UpdateFeedUserCommand + { + return new \Timetabio\API\Commands\Feed\UpdateFeedUserCommand( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + } +} diff --git a/API/src/Factories/ControllerFactory.php b/API/src/Factories/ControllerFactory.php new file mode 100644 index 0000000..8e1be39 --- /dev/null +++ b/API/src/Factories/ControllerFactory.php @@ -0,0 +1,618 @@ +getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetIndexQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetProfileController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Profile\ProfileModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetProfileRequestHandler(), + $this->getMasterFactory()->createGetProfileQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetRandomController(): GetController + { + return new GetController( + new \Timetabio\API\Models\APIModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetRandomQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetFeedController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Feed\FeedModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetFeedRequestHandler(), + $this->getMasterFactory()->createGetFeedQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetFeedsController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetFeedsQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserController(): GetController + { + return new GetController( + new \Timetabio\API\Models\APIModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetUserQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserFeedsController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetUserFeedsQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateFeedController(): PatchController + { + return new PatchController( + new \Timetabio\API\Models\Feed\UpdateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateFeedRequestHandler(), + $this->getMasterFactory()->createUpdateFeedQueryHandler(), + $this->getMasterFactory()->createUpdateFeedCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateUserController(): PatchController + { + return new PatchController( + new \Timetabio\API\Models\User\UpdateUserModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateUserRequestHandler(), + $this->getMasterFactory()->createUpdateUserQueryHandler(), + $this->getMasterFactory()->createUpdateUserCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateUserPasswordController(): PutController + { + return new PutController( + new \Timetabio\API\Models\User\UpdateUserPasswordModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateUserPasswordRequestHandler(), + $this->getMasterFactory()->createUpdateUserPasswordQueryHandler(), + $this->getMasterFactory()->createUpdateUserPasswordCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createAuthController(): PostController + { + return new PostController( + new \Timetabio\API\Models\AuthModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createAuthRequestHandler(), + $this->getMasterFactory()->createAuthQueryHandler(), + $this->getMasterFactory()->createAuthCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetCollectionController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Collection\CollectionModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetCollectionRequestHandler(), + $this->getMasterFactory()->createGetCollectionQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createDeleteCollectionController(): DeleteController + { + return new DeleteController( + new \Timetabio\API\Models\Collection\CollectionModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createDeleteCollectionRequestHandler(), + $this->getMasterFactory()->createDeleteCollectionQueryHandler(), + $this->getMasterFactory()->createDeleteCollectionCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateCollectionController(): PatchController + { + return new PatchController( + new \Timetabio\API\Models\Collection\UpdateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateCollectionRequestHandler(), + $this->getMasterFactory()->createUpdateCollectionQueryHandler(), + $this->getMasterFactory()->createUpdateCollectionCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserCollectionsController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetUserCollectionsQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createFollowFeedController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Feed\FollowModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createFollowFeedRequestHandler(), + $this->getMasterFactory()->createFollowFeedQueryHandler(), + $this->getMasterFactory()->createFollowFeedCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUnfollowFeedController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Feed\FollowModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createFollowFeedRequestHandler(), + $this->getMasterFactory()->createUnfollowFeedQueryHandler(), + $this->getMasterFactory()->createUnfollowFeedCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateFeedController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Feed\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateFeedRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateFeedCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateUserController(): PostController + { + return new PostController( + new \Timetabio\API\Models\User\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateUserRequestHandler(), + $this->getMasterFactory()->createCreateUserQueryHandler(), + $this->getMasterFactory()->createCreateUserCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createVerifyController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Verify\VerifyModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createVerifyRequestHandler(), + $this->getMasterFactory()->createVerifyQueryHandler(), + $this->getMasterFactory()->createVerifyCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createResendVerificationController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Verify\ResendModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createResendVerificationRequestHandler(), + $this->getMasterFactory()->createResendVerificationQueryHandler(), + $this->getMasterFactory()->createResendVerificationCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateCollectionController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Collection\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateCollectionRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateCollectionCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createDeleteFeedPersonController(): DeleteController + { + return new DeleteController( + new \Timetabio\API\Models\Feed\People\DeleteModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createDeleteFeedPeopleRequestHandler(), + $this->getMasterFactory()->createDeleteFeedPeopleQueryHandler(), + $this->getMasterFactory()->createDeleteFeedPeopleCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetFeedUsersController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Feed\People\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetFeedPeopleRequestHandler(), + $this->getMasterFactory()->createGetFeedPeopleQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreatePostController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Post\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreatePostRequestHandler(), + $this->getMasterFactory()->createCreatePostQueryHandler(), + $this->getMasterFactory()->createCreatePostCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetFeedPostsController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Feed\Posts\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetFeedPostsRequestHandler(), + $this->getMasterFactory()->createGetFeedPostsQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetPostController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Post\PostModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetPostRequestHandler(), + $this->getMasterFactory()->createGetPostQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserUpcomingController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetUserUpcomingQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserTodoController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetUserTodoQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createDeletePostController(): DeleteController + { + return new DeleteController( + new \Timetabio\API\Models\Post\PostModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createDeletePostRequestHandler(), + $this->getMasterFactory()->createDeletePostQueryHandler(), + $this->getMasterFactory()->createDeletePostCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateFeedUploadUrlController(): DeleteController + { + return new DeleteController( + new \Timetabio\API\Models\Feed\UploadModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateFeedUploadRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateFeedUploadCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createRevokeController(): PostController + { + return new PostController( + new \Timetabio\API\Models\APIModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createRevokeCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateBetaRequestController(): PostController + { + return new PostController( + new \Timetabio\API\Models\BetaRequest\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateBetaRequestRequestHandler(), + $this->getMasterFactory()->createCreateBetaRequestQueryHandler(), + $this->getMasterFactory()->createCreateBetaRequestCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createCreateFeedInvitationController(): PostController + { + return new PostController( + new \Timetabio\API\Models\Feed\Invitation\CreateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createCreateFeedInvitationRequestHandler(), + $this->getMasterFactory()->createCreateFeedInvitationQueryHandler(), + $this->getMasterFactory()->createCreateFeedInvitationCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetFeedInvitationsController(): GetController + { + return new GetController( + new \Timetabio\API\Models\Feed\FeedModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createGetFeedRequestHandler(), + $this->getMasterFactory()->createGetFeedInvitationsQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateFeedInvitationController(): PatchController + { + return new PatchController( + new \Timetabio\API\Models\Feed\Invitation\UpdateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateFeedInvitationRequestHandler(), + $this->getMasterFactory()->createUpdateFeedInvitationQueryHandler(), + $this->getMasterFactory()->createUpdateFeedInvitationCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createDeleteFeedInvitationController(): DeleteController + { + return new DeleteController( + new \Timetabio\API\Models\Feed\Invitation\DeleteModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createDeleteFeedInvitationRequestHandler(), + $this->getMasterFactory()->createDeleteFeedInvitationQueryHandler(), + $this->getMasterFactory()->createDeleteFeedInvitationCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createUpdateFeedUserController(): PatchController + { + return new PatchController( + new \Timetabio\API\Models\Feed\User\UpdateModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createUpdateFeedUserRequestHandler(), + $this->getMasterFactory()->createUpdateFeedUserQueryHandler(), + $this->getMasterFactory()->createUpdateFeedUserCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createSearchController(): GetController + { + return new GetController( + new \Timetabio\API\Models\SearchModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createSearchRequestHandler(), + $this->getMasterFactory()->createSearchQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + + public function createGetUserFeedController(): GetController + { + return new GetController( + new \Timetabio\API\Models\ListModel, + $this->getMasterFactory()->createPreHandler(), + $this->getMasterFactory()->createListRequestHandler(), + $this->getMasterFactory()->createGetUserFeedQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new JsonResponse + ); + } + } +} diff --git a/API/src/Factories/EndpointFactory.php b/API/src/Factories/EndpointFactory.php new file mode 100644 index 0000000..d2d7e86 --- /dev/null +++ b/API/src/Factories/EndpointFactory.php @@ -0,0 +1,298 @@ +getMasterFactory() + ); + } + + public function createCreateUserEndpoint(): \Timetabio\API\Endpoints\Users\CreateUserEndpoint + { + return new \Timetabio\API\Endpoints\Users\CreateUserEndpoint( + $this->getMasterFactory() + ); + } + + public function createAuthEndpoint(): \Timetabio\API\Endpoints\AuthEndpoint + { + return new \Timetabio\API\Endpoints\AuthEndpoint( + $this->getMasterFactory() + ); + } + + public function createVerifyEndpoint(): \Timetabio\API\Endpoints\Verify\VerifyEndpoint + { + return new \Timetabio\API\Endpoints\Verify\VerifyEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUserEndpoint(): \Timetabio\API\Endpoints\User\GetUserEndpoint + { + return new \Timetabio\API\Endpoints\User\GetUserEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateUserEndpoint(): \Timetabio\API\Endpoints\User\UpdateUserEndpoint + { + return new \Timetabio\API\Endpoints\User\UpdateUserEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateUserPasswordEndpoint(): \Timetabio\API\Endpoints\User\UpdateUserPasswordEndpoint + { + return new \Timetabio\API\Endpoints\User\UpdateUserPasswordEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetProfileEndpoint(): \Timetabio\API\Endpoints\Profiles\GetProfileEndpoint + { + return new \Timetabio\API\Endpoints\Profiles\GetProfileEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetCollectionEndpoint(): \Timetabio\API\Endpoints\Collections\GetCollectionEndpoint + { + return new \Timetabio\API\Endpoints\Collections\GetCollectionEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateCollectionEndpoint(): \Timetabio\API\Endpoints\Collections\UpdateCollectionEndpoint + { + return new \Timetabio\API\Endpoints\Collections\UpdateCollectionEndpoint( + $this->getMasterFactory() + ); + } + + public function createDeleteCollectionEndpoint(): \Timetabio\API\Endpoints\Collections\DeleteCollectionEndpoint + { + return new \Timetabio\API\Endpoints\Collections\DeleteCollectionEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreateFeedEndpoint(): \Timetabio\API\Endpoints\Feeds\CreateFeedEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\CreateFeedEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreateCollectionEndpoint(): \Timetabio\API\Endpoints\Collections\CreateCollectionEndpoint + { + return new \Timetabio\API\Endpoints\Collections\CreateCollectionEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetFeedsEndpoint(): \Timetabio\API\Endpoints\Feeds\GetFeedsEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\GetFeedsEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetFeedEndpoint(): \Timetabio\API\Endpoints\Feeds\GetFeedEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\GetFeedEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUserFeedsEndpoint(): \Timetabio\API\Endpoints\User\GetUserFeedsEndpoint + { + return new \Timetabio\API\Endpoints\User\GetUserFeedsEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateFeedEndpoint(): \Timetabio\API\Endpoints\Feeds\UpdateFeedEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\UpdateFeedEndpoint( + $this->getMasterFactory() + ); + } + + public function createFollowFeedEndpoint(): \Timetabio\API\Endpoints\Feeds\FollowFeedEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\FollowFeedEndpoint( + $this->getMasterFactory() + ); + } + + public function createUnfollowFeedEndpoint(): \Timetabio\API\Endpoints\Feeds\UnfollowFeedEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\UnfollowFeedEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetRandomEndpoint(): \Timetabio\API\Endpoints\Random\GetRandomEndpoint + { + return new \Timetabio\API\Endpoints\Random\GetRandomEndpoint( + $this->getMasterFactory() + ); + } + + public function createResendVerificationEndpoint(): \Timetabio\API\Endpoints\Verify\ResendEndpoint + { + return new \Timetabio\API\Endpoints\Verify\ResendEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUserCollectionsEndpoint(): \Timetabio\API\Endpoints\User\GetUserCollectionsEndpoint + { + return new \Timetabio\API\Endpoints\User\GetUserCollectionsEndpoint( + $this->getMasterFactory() + ); + } + + public function createDeleteFeedUserEndpoint(): \Timetabio\API\Endpoints\Feeds\DeleteFeedUserEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\DeleteFeedUserEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetFeedUsersEndpoint(): \Timetabio\API\Endpoints\Feeds\GetFeedUsersEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\GetFeedUsersEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreatePostEndpoint(): \Timetabio\API\Endpoints\Posts\CreatePostEndpoint + { + return new \Timetabio\API\Endpoints\Posts\CreatePostEndpoint( + $this->getMasterFactory() + ); + } + + public function createDeletePostEndpoint(): \Timetabio\API\Endpoints\Posts\DeletePostEndpoint + { + return new \Timetabio\API\Endpoints\Posts\DeletePostEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetPostEndpoint(): \Timetabio\API\Endpoints\Posts\GetPostEndpoint + { + return new \Timetabio\API\Endpoints\Posts\GetPostEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetPostsEndpoint(): \Timetabio\API\Endpoints\Posts\GetPostsEndpoint + { + return new \Timetabio\API\Endpoints\Posts\GetPostsEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdatePostEndpoint(): \Timetabio\API\Endpoints\Posts\UpdatePostEndpoint + { + return new \Timetabio\API\Endpoints\Posts\UpdatePostEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUpcomingEndpoint(): \Timetabio\API\Endpoints\User\GetUpcomingEndpoint + { + return new \Timetabio\API\Endpoints\User\GetUpcomingEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUserTodoEndpoint(): \Timetabio\API\Endpoints\User\GetTodoEndpoint + { + return new \Timetabio\API\Endpoints\User\GetTodoEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreateFeedUploadUrlEndpoint(): \Timetabio\API\Endpoints\Feeds\CreateUploadEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\CreateUploadEndpoint( + $this->getMasterFactory() + ); + } + + public function createRevokeEndpoint(): \Timetabio\API\Endpoints\RevokeEndpoint + { + return new \Timetabio\API\Endpoints\RevokeEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreateBetaRequestEndpoint(): \Timetabio\API\Endpoints\BetaRequest\CreateBetaRequestEndpoint + { + return new \Timetabio\API\Endpoints\BetaRequest\CreateBetaRequestEndpoint( + $this->getMasterFactory() + ); + } + + public function createCreateFeedInvitationEndpoint(): \Timetabio\API\Endpoints\Feeds\CreateInvitationEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\CreateInvitationEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetFeedInvitationsEndpoint(): \Timetabio\API\Endpoints\Feeds\GetFeedInvitationsEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\GetFeedInvitationsEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateFeedInvitationEndpoint(): \Timetabio\API\Endpoints\Feeds\UpdateFeedInvitationEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\UpdateFeedInvitationEndpoint( + $this->getMasterFactory() + ); + } + + public function createDeleteFeedInvitationEndpoint(): \Timetabio\API\Endpoints\Feeds\DeleteFeedInvitationEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\DeleteFeedInvitationEndpoint( + $this->getMasterFactory() + ); + } + + public function createUpdateFeedUserEndpoint(): \Timetabio\API\Endpoints\Feeds\UpdateFeedUserEndpoint + { + return new \Timetabio\API\Endpoints\Feeds\UpdateFeedUserEndpoint( + $this->getMasterFactory() + ); + } + + public function createSearchEndpoint(): \Timetabio\API\Endpoints\SearchEndpoint + { + return new \Timetabio\API\Endpoints\SearchEndpoint( + $this->getMasterFactory() + ); + } + + public function createGetUserFeedEndpoint(): \Timetabio\API\Endpoints\User\GetUserFeedEndpoint + { + return new \Timetabio\API\Endpoints\User\GetUserFeedEndpoint( + $this->getMasterFactory() + ); + } + } +} diff --git a/API/src/Factories/ErrorHandlerFactory.php b/API/src/Factories/ErrorHandlerFactory.php new file mode 100644 index 0000000..cea377a --- /dev/null +++ b/API/src/Factories/ErrorHandlerFactory.php @@ -0,0 +1,21 @@ +getMasterFactory()->createDataStoreWriter() + ); + } + + public function createPreHandler(): \Timetabio\API\Handlers\PreHandler + { + return new \Timetabio\API\Handlers\PreHandler( + $this->getMasterFactory()->createDataStoreReader(), + $this->getMasterFactory()->createRequestTokenReader() + ); + } + + public function createRequestHandler(): \Timetabio\API\Handlers\RequestHandler + { + return new \Timetabio\API\Handlers\RequestHandler; + } + + public function createQueryHandler(): \Timetabio\API\Handlers\QueryHandler + { + return new \Timetabio\API\Handlers\QueryHandler; + } + + public function createResponseHandler(): \Timetabio\API\Handlers\ResponseHandler + { + return new \Timetabio\API\Handlers\ResponseHandler; + } + + public function createTransformationHandler(): \Timetabio\API\Handlers\TransformationHandler + { + return new \Timetabio\API\Handlers\TransformationHandler; + } + + public function createGetIndexQueryHandler(): \Timetabio\API\Handlers\Get\Index\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Index\QueryHandler; + } + + public function createCreateUserRequestHandler(): \Timetabio\API\Handlers\Post\Users\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Users\RequestHandler; + } + + public function createCreateUserQueryHandler(): \Timetabio\API\Handlers\Post\Users\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Users\QueryHandler( + $this->getMasterFactory()->createFetchUserByEmailQuery(), + $this->getMasterFactory()->createFetchUserByUsernameQuery(), + $this->getMasterFactory()->createIsInvitedQuery() + ); + } + + public function createCreateUserCommandHandler(): \Timetabio\API\Handlers\Post\Users\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Users\CommandHandler( + $this->getMasterFactory()->createCreateUserCommand(), + $this->getMasterFactory()->createSendVerificationCommand(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createVerifyRequestHandler(): \Timetabio\API\Handlers\Post\Verify\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Verify\RequestHandler; + } + + public function createVerifyCommandHandler(): \Timetabio\API\Handlers\Post\Verify\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Verify\CommandHandler( + $this->getMasterFactory()->createVerifyUserCommand() + ); + } + + public function createVerifyQueryHandler(): \Timetabio\API\Handlers\Post\Verify\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Verify\QueryHandler( + $this->getMasterFactory()->createFetchVerificationTokenQuery() + ); + } + + public function createAuthRequestHandler(): \Timetabio\API\Handlers\Post\Auth\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Auth\RequestHandler; + } + + public function createAuthQueryHandler(): \Timetabio\API\Handlers\Post\Auth\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Auth\QueryHandler( + $this->getMasterFactory()->createFetchAuthUserQuery() + ); + } + + public function createAuthCommandHandler(): \Timetabio\API\Handlers\Post\Auth\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Auth\CommandHandler( + $this->getMasterFactory()->createSaveAccessTokenCommand() + ); + } + + public function createGetUserQueryHandler(): \Timetabio\API\Handlers\Get\User\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\QueryHandler( + $this->getMasterFactory()->createFetchUserByIdQuery(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createUpdateUserRequestHandler(): \Timetabio\API\Handlers\Patch\User\RequestHandler + { + return new \Timetabio\API\Handlers\Patch\User\RequestHandler; + } + + public function createUpdateUserPasswordRequestHandler(): \Timetabio\API\Handlers\Put\User\RequestHandler + { + return new \Timetabio\API\Handlers\Put\User\RequestHandler; + } + + public function createUpdateUserQueryHandler(): \Timetabio\API\Handlers\Patch\User\QueryHandler + { + return new \Timetabio\API\Handlers\Patch\User\QueryHandler( + $this->getMasterFactory()->createFetchUsernameQuery() + ); + } + + public function createUpdateUserCommandHandler(): \Timetabio\API\Handlers\Patch\User\CommandHandler + { + return new \Timetabio\API\Handlers\Patch\User\CommandHandler( + $this->getMasterFactory()->createUpdateUserCommand() + ); + } + + public function createUpdateUserPasswordQueryHandler(): \Timetabio\API\Handlers\Put\User\QueryHandler + { + return new \Timetabio\API\Handlers\Put\User\QueryHandler( + $this->getMasterFactory()->createFetchUserPasswordQuery() + ); + } + + public function createUpdateUserPasswordCommandHandler(): \Timetabio\API\Handlers\Put\User\CommandHandler + { + return new \Timetabio\API\Handlers\Put\User\CommandHandler( + $this->getMasterFactory()->createUpdateUserCommand() + ); + } + + public function createGetProfileRequestHandler(): \Timetabio\API\Handlers\Get\Profile\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Profile\RequestHandler; + } + + public function createGetProfileQueryHandler(): \Timetabio\API\Handlers\Get\Profile\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Profile\QueryHandler( + $this->getMasterFactory()->createFetchProfileQuery(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createCreateFeedRequestHandler(): \Timetabio\API\Handlers\Post\Feeds\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Feeds\RequestHandler; + } + + public function createCreateFeedCommandHandler(): \Timetabio\API\Handlers\Post\Feeds\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Feeds\CommandHandler( + $this->getMasterFactory()->createCreateFeedCommand(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createGetFeedsQueryHandler(): \Timetabio\API\Handlers\Get\Feeds\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Feeds\QueryHandler( + $this->getMasterFactory()->createFetchFeedsQuery(), + $this->getMasterFactory()->createFeedMapper() + ); + } + + public function createGetFeedRequestHandler(): \Timetabio\API\Handlers\Get\Feed\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Feed\RequestHandler; + } + + public function createGetFeedQueryHandler(): \Timetabio\API\Handlers\Get\Feed\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Feed\QueryHandler( + $this->getMasterFactory()->createFetchFeedQuery(), + $this->getMasterFactory()->createFetchFeedVanityQuery(), + $this->getMasterFactory()->createInvitationExistsQuery(), + $this->getMasterFactory()->createFeedMapper(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createGetUserFeedsQueryHandler(): \Timetabio\API\Handlers\Get\User\Feeds\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\Feeds\QueryHandler( + $this->getMasterFactory()->createFetchUserFeedsQuery(), + $this->getMasterFactory()->createResultsMapper() + ); + } + + public function createUpdateFeedRequestHandler(): \Timetabio\API\Handlers\Patch\Feed\RequestHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\RequestHandler; + } + + public function createUpdateFeedQueryHandler(): \Timetabio\API\Handlers\Patch\Feed\QueryHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\QueryHandler( + $this->getMasterFactory()->createFetchVanityByNameQuery(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createUpdateFeedCommandHandler(): \Timetabio\API\Handlers\Patch\Feed\CommandHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\CommandHandler( + $this->getMasterFactory()->createUpdateFeedCommand(), + $this->getMasterFactory()->createSetFeedVanityCommand() + ); + } + + public function createFollowFeedRequestHandler(): \Timetabio\API\Handlers\Post\Feed\FollowRequestHandler + { + return new \Timetabio\API\Handlers\Post\Feed\FollowRequestHandler; + } + + public function createFollowFeedQueryHandler(): \Timetabio\API\Handlers\Post\Feed\Follow\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Follow\QueryHandler( + $this->getMasterFactory()->createFetchFollowerQuery(), + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchInvitationQuery(), + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createUnfollowFeedQueryHandler(): \Timetabio\API\Handlers\Post\Feed\Unfollow\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Unfollow\QueryHandler( + $this->getMasterFactory()->createFetchFollowerQuery(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createFollowFeedCommandHandler(): \Timetabio\API\Handlers\Post\Feed\Follow\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Follow\CommandHandler( + $this->getMasterFactory()->createFollowFeedCommand(), + $this->getMasterFactory()->createDeleteInvitationCommand() + ); + } + + public function createUnfollowFeedCommandHandler(): \Timetabio\API\Handlers\Post\Feed\Unfollow\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Unfollow\CommandHandler( + $this->getMasterFactory()->createUnfollowFeedCommand() + ); + } + + public function createGetCollectionQueryHandler(): \Timetabio\API\Handlers\Get\Collection\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Collection\QueryHandler( + $this->getMasterFactory()->createFetchCollectionQuery(), + $this->getMasterFactory()->createDocumentMapper(), + $this->getMasterFactory()->createCollectionAccessControl() + ); + } + + public function createDeleteCollectionQueryHandler(): \Timetabio\API\Handlers\Delete\Collection\QueryHandler + { + return new \Timetabio\API\Handlers\Delete\Collection\QueryHandler( + $this->getMasterFactory()->createFetchCollectionQuery(), + $this->getMasterFactory()->createDocumentMapper(), + $this->getMasterFactory()->createCollectionAccessControl() + ); + } + + public function createDeleteCollectionCommandHandler(): \Timetabio\API\Handlers\Delete\Collection\CommandHandler + { + return new \Timetabio\API\Handlers\Delete\Collection\CommandHandler( + $this->getMasterFactory()->createDeleteCollectionCommand() + ); + } + + public function createUpdateCollectionQueryHandler(): \Timetabio\API\Handlers\Patch\Collection\QueryHandler + { + return new \Timetabio\API\Handlers\Patch\Collection\QueryHandler( + $this->getMasterFactory()->createFetchCollectionQuery(), + $this->getMasterFactory()->createCollectionAccessControl() + ); + } + + public function createGetCollectionRequestHandler(): \Timetabio\API\Handlers\Get\Collection\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Collection\RequestHandler; + } + + public function createDeleteCollectionRequestHandler(): \Timetabio\API\Handlers\Delete\Collection\RequestHandler + { + return new \Timetabio\API\Handlers\Delete\Collection\RequestHandler; + } + + public function createUpdateCollectionRequestHandler(): \Timetabio\API\Handlers\Patch\Collection\RequestHandler + { + return new \Timetabio\API\Handlers\Patch\Collection\RequestHandler; + } + + public function createGetUserCollectionsQueryHandler(): \Timetabio\API\Handlers\Get\User\Collections\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\Collections\QueryHandler( + $this->getMasterFactory()->createFetchUserCollectionsQuery(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createCreateCollectionRequestHandler(): \Timetabio\API\Handlers\Post\Collections\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Collections\RequestHandler; + } + + public function createCreateCollectionCommandHandler(): \Timetabio\API\Handlers\Post\Collections\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Collections\CommandHandler( + $this->getMasterFactory()->createCreateCollectionCommand(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createUpdateCollectionCommandHandler(): \Timetabio\API\Handlers\Patch\Collection\CommandHandler + { + return new \Timetabio\API\Handlers\Patch\Collection\CommandHandler( + $this->getMasterFactory()->createUpdateCollectionCommand() + ); + } + + public function createGetRandomQueryHandler(): \Timetabio\API\Handlers\Get\Random\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Random\QueryHandler; + } + + public function createResendVerificationRequestHandler(): \Timetabio\API\Handlers\Post\Verify\Resend\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Verify\Resend\RequestHandler; + } + + public function createResendVerificationQueryHandler(): \Timetabio\API\Handlers\Post\Verify\Resend\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Verify\Resend\QueryHandler( + $this->getMasterFactory()->createFetchVerificationTokenByEmailQuery() + ); + } + + public function createResendVerificationCommandHandler(): \Timetabio\API\Handlers\Post\Verify\Resend\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Verify\Resend\CommandHandler( + $this->getMasterFactory()->createSendVerificationCommand() + ); + } + + public function createListRequestHandler(): \Timetabio\API\Handlers\Get\ListRequestHandler + { + return new \Timetabio\API\Handlers\Get\ListRequestHandler; + } + + public function createDeleteFeedPeopleCommandHandler(): \Timetabio\API\Handlers\Delete\Feed\People\CommandHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\People\CommandHandler( + $this->getMasterFactory()->createDeleteFeedPersonCommand() + ); + } + + public function createDeleteFeedPeopleQueryHandler(): \Timetabio\API\Handlers\Delete\Feed\People\QueryHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\People\QueryHandler( + $this->getMasterFactory()->createFetchPersonQuery(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createDeleteFeedPeopleRequestHandler(): \Timetabio\API\Handlers\Delete\Feed\People\RequestHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\People\RequestHandler; + } + + public function createGetFeedPeopleRequestHandler(): \Timetabio\API\Handlers\Get\Feed\People\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Feed\People\RequestHandler; + } + + public function createGetFeedPeopleQueryHandler(): \Timetabio\API\Handlers\Get\Feed\People\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Feed\People\QueryHandler( + $this->getMasterFactory()->createFetchPeopleQuery(), + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFeedUserMapper() + ); + } + + public function createCreatePostRequestHandler(): \Timetabio\API\Handlers\Post\Posts\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Posts\RequestHandler( + $this->getMasterFactory()->createPostTypeLocator() + ); + } + + public function createCreatePostQueryHandler(): \Timetabio\API\Handlers\Post\Posts\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Posts\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchFileByPublicIdQuery() + ); + } + + public function createCreatePostCommandHandler(): \Timetabio\API\Handlers\Post\Posts\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Posts\CommandHandler( + $this->getMasterFactory()->createCreatePostCommand(), + $this->getMasterFactory()->createPostMapper() + ); + } + + public function createGetFeedPostsRequestHandler(): \Timetabio\API\Handlers\Get\Feed\Posts\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Feed\Posts\RequestHandler; + } + + public function createGetFeedPostsQueryHandler(): \Timetabio\API\Handlers\Get\Feed\Posts\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Feed\Posts\QueryHandler( + $this->getMasterFactory()->createFetchFeedPostsQuery(), + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createResultsMapper() + ); + } + + public function createGetPostRequestHandler(): \Timetabio\API\Handlers\Get\Post\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Post\RequestHandler; + } + + public function createGetPostQueryHandler(): \Timetabio\API\Handlers\Get\Post\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Post\QueryHandler( + $this->getMasterFactory()->createFetchPostQuery(), + $this->getMasterFactory()->createFetchPostAttachmentsQuery(), + $this->getMasterFactory()->createPostMapper(), + $this->getMasterFactory()->createPostAttachmentMapper(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createGetUserUpcomingQueryHandler(): \Timetabio\API\Handlers\Get\User\Upcoming\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\Upcoming\QueryHandler( + $this->getMasterFactory()->createFetchUpcomingEventsQuery(), + $this->getMasterFactory()->createPostMapper() + ); + } + + public function createGetUserTodoQueryHandler(): \Timetabio\API\Handlers\Get\User\Todo\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\Todo\QueryHandler( + $this->getMasterFactory()->createFetchUserTodoTasksQuery(), + $this->getMasterFactory()->createPostMapper() + ); + } + + public function createDeletePostCommandHandler(): \Timetabio\API\Handlers\Delete\Post\CommandHandler + { + return new \Timetabio\API\Handlers\Delete\Post\CommandHandler( + $this->getMasterFactory()->createDeletePostCommand() + ); + } + + public function createDeletePostQueryHandler(): \Timetabio\API\Handlers\Delete\Post\QueryHandler + { + return new \Timetabio\API\Handlers\Delete\Post\QueryHandler( + $this->getMasterFactory()->createFetchPostInfoQuery(), + $this->getMasterFactory()->createFeedAccessControl() + ); + } + + public function createDeletePostRequestHandler(): \Timetabio\API\Handlers\Delete\Post\RequestHandler + { + return new \Timetabio\API\Handlers\Delete\Post\RequestHandler; + } + + public function createCreateFeedUploadRequestHandler(): \Timetabio\API\Handlers\Post\Feed\Upload\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Upload\RequestHandler; + } + + public function createCreateFeedUploadCommandHandler(): \Timetabio\API\Handlers\Post\Feed\Upload\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Upload\CommandHandler( + $this->getMasterFactory()->createCreateFeedUploadUrlCommand(), + $this->getMasterFactory()->createCreateFileCommand() + ); + } + + public function createRevokeCommandHandler(): \Timetabio\API\Handlers\Post\Revoke\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Revoke\CommandHandler( + $this->getMasterFactory()->createDeleteAccessTokenCommand() + ); + } + + public function createCreateBetaRequestRequestHandler(): \Timetabio\API\Handlers\Post\BetaRequest\RequestHandler + { + return new \Timetabio\API\Handlers\Post\BetaRequest\RequestHandler; + } + + public function createCreateBetaRequestCommandHandler(): \Timetabio\API\Handlers\Post\BetaRequest\CommandHandler + { + return new \Timetabio\API\Handlers\Post\BetaRequest\CommandHandler( + $this->getMasterFactory()->createCreateBetaRequestCommand() + ); + } + + public function createCreateBetaRequestQueryHandler(): \Timetabio\API\Handlers\Post\BetaRequest\QueryHandler + { + return new \Timetabio\API\Handlers\Post\BetaRequest\QueryHandler( + $this->getMasterFactory()->createFetchBetaRequestByEmailQuery() + ); + } + + public function createCreateFeedInvitationRequestHandler(): \Timetabio\API\Handlers\Post\Feed\Invitations\RequestHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Invitations\RequestHandler( + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createCreateFeedInvitationQueryHandler(): \Timetabio\API\Handlers\Post\Feed\Invitations\QueryHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Invitations\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createInvitationExistsQuery(), + $this->getMasterFactory()->createFetchFeedUserQuery(), + $this->getMasterFactory()->createFetchUserByUsernameQuery() + ); + } + + public function createCreateFeedInvitationCommandHandler(): \Timetabio\API\Handlers\Post\Feed\Invitations\CommandHandler + { + return new \Timetabio\API\Handlers\Post\Feed\Invitations\CommandHandler( + $this->getMasterFactory()->createCreateInvitationCommand(), + $this->getMasterFactory()->createDocumentMapper() + ); + } + + public function createGetFeedInvitationsQueryHandler(): \Timetabio\API\Handlers\Get\Feed\Invitations\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Feed\Invitations\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchInvitationsQuery(), + $this->getMasterFactory()->createFeedUserMapper() + ); + } + + public function createUpdateFeedInvitationRequestHandler(): \Timetabio\API\Handlers\Patch\Feed\Invitation\RequestHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\Invitation\RequestHandler( + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createUpdateFeedInvitationQueryHandler(): \Timetabio\API\Handlers\Patch\Feed\Invitation\QueryHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\Invitation\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchInvitationQuery() + ); + } + + public function createUpdateFeedInvitationCommandHandler(): \Timetabio\API\Handlers\Patch\Feed\Invitation\CommandHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\Invitation\CommandHandler( + $this->getMasterFactory()->createUpdateInvitationCommand() + ); + } + + public function createDeleteFeedInvitationRequestHandler(): \Timetabio\API\Handlers\Delete\Feed\Invitation\RequestHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\Invitation\RequestHandler; + } + + public function createDeleteFeedInvitationQueryHandler(): \Timetabio\API\Handlers\Delete\Feed\Invitation\QueryHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\Invitation\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchInvitationQuery() + ); + } + + public function createDeleteFeedInvitationCommandHandler(): \Timetabio\API\Handlers\Delete\Feed\Invitation\CommandHandler + { + return new \Timetabio\API\Handlers\Delete\Feed\Invitation\CommandHandler( + $this->getMasterFactory()->createDeleteInvitationCommand() + ); + } + + public function createUpdateFeedUserRequestHandler(): \Timetabio\API\Handlers\Patch\Feed\User\RequestHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\User\RequestHandler( + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createUpdateFeedUserQueryHandler(): \Timetabio\API\Handlers\Patch\Feed\User\QueryHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\User\QueryHandler( + $this->getMasterFactory()->createFeedAccessControl(), + $this->getMasterFactory()->createFetchFeedUserQuery() + ); + } + + public function createUpdateFeedUserCommandHandler(): \Timetabio\API\Handlers\Patch\Feed\User\CommandHandler + { + return new \Timetabio\API\Handlers\Patch\Feed\User\CommandHandler( + $this->getMasterFactory()->createUpdateFeedUserCommand() + ); + } + + public function createSearchRequestHandler(): \Timetabio\API\Handlers\Get\Search\RequestHandler + { + return new \Timetabio\API\Handlers\Get\Search\RequestHandler( + $this->getMasterFactory()->createSearchTypeLocator() + ); + } + + public function createSearchQueryHandler(): \Timetabio\API\Handlers\Get\Search\QueryHandler + { + return new \Timetabio\API\Handlers\Get\Search\QueryHandler( + $this->getMasterFactory()->createSearchQuery(), + $this->getMasterFactory()->createSearchResultsMapper() + ); + } + + public function createGetUserFeedQueryHandler(): \Timetabio\API\Handlers\Get\User\Feed\QueryHandler + { + return new \Timetabio\API\Handlers\Get\User\Feed\QueryHandler( + $this->getMasterFactory()->createFetchUserFeedQuery(), + $this->getMasterFactory()->createResultsMapper() + ); + } + } +} diff --git a/API/src/Factories/MapperFactory.php b/API/src/Factories/MapperFactory.php new file mode 100644 index 0000000..3a79097 --- /dev/null +++ b/API/src/Factories/MapperFactory.php @@ -0,0 +1,35 @@ +getMasterFactory()->createDocumentMapper() + ); + } + + public function createPostAttachmentMapper(): \Timetabio\API\Mappers\PostAttachmentMapper + { + return new \Timetabio\API\Mappers\PostAttachmentMapper( + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createSearchResultsMapper(): \Timetabio\API\Mappers\SearchResultsMapper + { + return new \Timetabio\API\Mappers\SearchResultsMapper; + } + + public function createResultsMapper(): \Timetabio\API\Mappers\ResultsMapper + { + return new \Timetabio\API\Mappers\ResultsMapper; + } + } +} diff --git a/API/src/Factories/QueryFactory.php b/API/src/Factories/QueryFactory.php new file mode 100644 index 0000000..99d342d --- /dev/null +++ b/API/src/Factories/QueryFactory.php @@ -0,0 +1,257 @@ +getMasterFactory()->createUserService() + ); + } + + public function createFetchVerificationTokenQuery(): \Timetabio\API\Queries\User\FetchVerificationTokenQuery + { + return new \Timetabio\API\Queries\User\FetchVerificationTokenQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchUserByIdQuery(): \Timetabio\API\Queries\User\FetchUserByIdQuery + { + return new \Timetabio\API\Queries\User\FetchUserByIdQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchUserByUsernameQuery(): \Timetabio\API\Queries\User\FetchUserByUsernameQuery + { + return new \Timetabio\API\Queries\User\FetchUserByUsernameQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchProfileQuery(): \Timetabio\API\Queries\Profile\FetchProfileQuery + { + return new \Timetabio\API\Queries\Profile\FetchProfileQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchUsernameQuery(): \Timetabio\API\Queries\User\FetchUsernameQuery + { + return new \Timetabio\API\Queries\User\FetchUsernameQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchAuthUserQuery(): \Timetabio\API\Queries\User\FetchAuthUserQuery + { + return new \Timetabio\API\Queries\User\FetchAuthUserQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchUserPasswordQuery(): \Timetabio\API\Queries\User\FetchUserPasswordQuery + { + return new \Timetabio\API\Queries\User\FetchUserPasswordQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchCollectionQuery(): \Timetabio\API\Queries\FetchCollectionQuery + { + return new \Timetabio\API\Queries\FetchCollectionQuery( + $this->getMasterFactory()->createCollectionService() + ); + } + + public function createIsInvitedQuery(): \Timetabio\API\Queries\User\IsInvitedQuery + { + return new \Timetabio\API\Queries\User\IsInvitedQuery( + $this->getMasterFactory()->createBetaRequestService() + ); + } + + public function createFetchFeedsQuery(): \Timetabio\API\Queries\Feeds\FetchFeedsQuery + { + return new \Timetabio\API\Queries\Feeds\FetchFeedsQuery( + $this->getMasterFactory()->createFeedService() + ); + } + + public function createFetchFeedQuery(): \Timetabio\API\Queries\Feeds\FetchFeedQuery + { + return new \Timetabio\API\Queries\Feeds\FetchFeedQuery( + $this->getMasterFactory()->createFeedService() + ); + } + + public function createFetchUserFeedsQuery(): \Timetabio\API\Queries\User\FetchUserFeedsQuery + { + return new \Timetabio\API\Queries\User\FetchUserFeedsQuery( + $this->getMasterFactory()->createSearchBackend() + ); + } + + public function createFetchFollowerQuery(): \Timetabio\API\Queries\Feeds\FetchFollowerQuery + { + return new \Timetabio\API\Queries\Feeds\FetchFollowerQuery( + $this->getMasterFactory()->createFollowerService() + ); + } + + public function createFetchUserCollectionsQuery(): \Timetabio\API\Queries\User\FetchUserCollectionsQuery + { + return new \Timetabio\API\Queries\User\FetchUserCollectionsQuery( + $this->getMasterFactory()->createCollectionService() + ); + } + + public function createFetchPersonQuery(): \Timetabio\API\Queries\Feeds\FetchPersonQuery + { + return new \Timetabio\API\Queries\Feeds\FetchPersonQuery( + $this->getMasterFactory()->createPeopleService() + ); + } + + public function createFetchPeopleQuery(): \Timetabio\API\Queries\Feeds\FetchPeopleQuery + { + return new \Timetabio\API\Queries\Feeds\FetchPeopleQuery( + $this->getMasterFactory()->createPeopleService() + ); + } + + public function createFetchFeedPostsQuery(): \Timetabio\API\Queries\Posts\FetchFeedPostsQuery + { + return new \Timetabio\API\Queries\Posts\FetchFeedPostsQuery( + $this->getMasterFactory()->createSearchBackend() + ); + } + + public function createFetchPostQuery(): \Timetabio\API\Queries\Posts\FetchPostQuery + { + return new \Timetabio\API\Queries\Posts\FetchPostQuery( + $this->getMasterFactory()->createPostService(), + $this->getMasterFactory()->createDataStoreReader() + ); + } + + public function createFetchVerificationTokenByEmailQuery(): \Timetabio\API\Queries\User\FetchVerificationTokenByEmailQuery + { + return new \Timetabio\API\Queries\User\FetchVerificationTokenByEmailQuery( + $this->getMasterFactory()->createUserService() + ); + } + + public function createFetchUpcomingEventsQuery(): \Timetabio\API\Queries\User\FetchUpcomingEventsQuery + { + return new \Timetabio\API\Queries\User\FetchUpcomingEventsQuery( + $this->getMasterFactory()->createPostService() + ); + } + + public function createFetchUserTodoTasksQuery(): \Timetabio\API\Queries\User\FetchTodoTasksQuery + { + return new \Timetabio\API\Queries\User\FetchTodoTasksQuery( + $this->getMasterFactory()->createPostService() + ); + } + + public function createFetchPostInfoQuery(): \Timetabio\API\Queries\Post\FetchPostInfoQuery + { + return new \Timetabio\API\Queries\Post\FetchPostInfoQuery( + $this->getMasterFactory()->createPostService() + ); + } + + public function createFetchVanityByNameQuery(): \Timetabio\API\Queries\Feed\FetchVanityByNameQuery + { + return new \Timetabio\API\Queries\Feed\FetchVanityByNameQuery( + $this->getMasterFactory()->createFeedService() + ); + } + + public function createFeedExistsQuery(): \Timetabio\API\Queries\Feed\FeedExistsQuery + { + return new \Timetabio\API\Queries\Feed\FeedExistsQuery( + $this->getMasterFactory()->createDataStoreReader() + ); + } + + public function createFetchFileByPublicIdQuery(): \Timetabio\API\Queries\File\FetchFileByPublicIdQuery + { + return new \Timetabio\API\Queries\File\FetchFileByPublicIdQuery( + $this->getMasterFactory()->createFileService() + ); + } + + public function createFetchPostAttachmentsQuery(): \Timetabio\API\Queries\Post\FetchPostAttachmentsQuery + { + return new \Timetabio\API\Queries\Post\FetchPostAttachmentsQuery( + $this->getMasterFactory()->createPostService() + ); + } + + public function createFetchBetaRequestByEmailQuery(): \Timetabio\API\Queries\BetaRequest\FetchBetaRequestByEmailQuery + { + return new \Timetabio\API\Queries\BetaRequest\FetchBetaRequestByEmailQuery( + $this->getMasterFactory()->createBetaRequestService() + ); + } + + public function createFetchInvitationQuery(): \Timetabio\API\Queries\Feed\FetchInvitationQuery + { + return new \Timetabio\API\Queries\Feed\FetchInvitationQuery( + $this->getMasterFactory()->createFeedInvitationService() + ); + } + + public function createInvitationExistsQuery(): \Timetabio\API\Queries\Feed\InvitationExistsQuery + { + return new \Timetabio\API\Queries\Feed\InvitationExistsQuery( + $this->getMasterFactory()->createFeedInvitationService() + ); + } + + public function createFetchInvitationsQuery(): \Timetabio\API\Queries\Feed\FetchInvitationsQuery + { + return new \Timetabio\API\Queries\Feed\FetchInvitationsQuery( + $this->getMasterFactory()->createFeedInvitationService() + ); + } + + public function createFetchFeedUserQuery(): \Timetabio\API\Queries\Feed\FetchFeedUserQuery + { + return new \Timetabio\API\Queries\Feed\FetchFeedUserQuery( + $this->getMasterFactory()->createPeopleService() + ); + } + + public function createSearchQuery(): \Timetabio\API\Queries\SearchQuery + { + return new \Timetabio\API\Queries\SearchQuery( + $this->getMasterFactory()->createSearchBackend() + ); + } + + public function createFetchUserFeedQuery(): \Timetabio\API\Queries\User\FetchUserFeedQuery + { + return new \Timetabio\API\Queries\User\FetchUserFeedQuery( + $this->getMasterFactory()->createSearchBackend() + ); + } + + public function createFetchFeedVanityQuery(): \Timetabio\API\Queries\Feed\FetchFeedVanityQuery + { + return new \Timetabio\API\Queries\Feed\FetchFeedVanityQuery( + $this->getMasterFactory()->createDataStoreReader() + ); + } + } +} diff --git a/API/src/Factories/RouterFactory.php b/API/src/Factories/RouterFactory.php new file mode 100644 index 0000000..d1d937d --- /dev/null +++ b/API/src/Factories/RouterFactory.php @@ -0,0 +1,63 @@ +getMasterFactory()->createAccessControl() + ); + + $router->registerEndpoint($this->getMasterFactory()->createGetIndexEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateUserEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createAuthEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createVerifyEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateUserPasswordEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateUserEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetProfileEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetCollectionEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateCollectionEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateFeedEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetFeedsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserFeedsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserFeedsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetFeedEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateFeedEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createFollowFeedEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUnfollowFeedEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetRandomEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createResendVerificationEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserCollectionsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createDeleteFeedUserEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetFeedUsersEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreatePostEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createDeletePostEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetPostEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetPostsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdatePostEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateCollectionEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createDeleteCollectionEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUpcomingEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserTodoEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateFeedUploadUrlEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createRevokeEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateBetaRequestEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createCreateFeedInvitationEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetFeedInvitationsEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateFeedInvitationEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createDeleteFeedInvitationEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createUpdateFeedUserEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createSearchEndpoint()); + $router->registerEndpoint($this->getMasterFactory()->createGetUserFeedEndpoint()); + + return $router; + } + } +} diff --git a/API/src/Factories/ServiceFactory.php b/API/src/Factories/ServiceFactory.php new file mode 100644 index 0000000..f5ad806 --- /dev/null +++ b/API/src/Factories/ServiceFactory.php @@ -0,0 +1,67 @@ +getMasterFactory()->createPostgresBackend() + ); + } + + public function createCollectionService(): \Timetabio\API\Services\CollectionService + { + return new \Timetabio\API\Services\CollectionService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createFeedService(): \Timetabio\API\Services\FeedService + { + return new \Timetabio\API\Services\FeedService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createFollowerService(): \Timetabio\API\Services\FollowerService + { + return new \Timetabio\API\Services\FollowerService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createPeopleService(): \Timetabio\API\Services\PeopleService + { + return new \Timetabio\API\Services\PeopleService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createPostService(): \Timetabio\API\Services\PostService + { + return new \Timetabio\API\Services\PostService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createFileService(): \Timetabio\API\Services\FileService + { + return new \Timetabio\API\Services\FileService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createBetaRequestService(): \Timetabio\API\Services\BetaRequestService + { + return new \Timetabio\API\Services\BetaRequestService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + } +} diff --git a/API/src/Handlers/CommandHandler.php b/API/src/Handlers/CommandHandler.php new file mode 100644 index 0000000..7ffbf20 --- /dev/null +++ b/API/src/Handlers/CommandHandler.php @@ -0,0 +1,17 @@ +deleteCollectionCommand = $deleteCollectionCommand; + } + + public function execute(AbstractModel $model) + { + /** @var CollectionModel $model */ + + $this->deleteCollectionCommand->execute($model->getCollectionId()); + } + } +} diff --git a/API/src/Handlers/Delete/Collection/QueryHandler.php b/API/src/Handlers/Delete/Collection/QueryHandler.php new file mode 100644 index 0000000..92f6c43 --- /dev/null +++ b/API/src/Handlers/Delete/Collection/QueryHandler.php @@ -0,0 +1,56 @@ +fetchCollectionQuery = $fetchCollectionQuery; + $this->documentMapper = $documentMapper; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var CollectionModel $model */ + + $collection = $this->fetchCollectionQuery->execute($model->getCollectionId()); + + if ($collection === null || !$this->accessControl->hasWriteAccess($model->getAccessToken(), $collection)) { + throw new NotFound('collection not found', 'not_found'); + } + + $model->setData($this->documentMapper->map($collection)); + } + } +} diff --git a/API/src/Handlers/Delete/Collection/RequestHandler.php b/API/src/Handlers/Delete/Collection/RequestHandler.php new file mode 100644 index 0000000..69f9eb2 --- /dev/null +++ b/API/src/Handlers/Delete/Collection/RequestHandler.php @@ -0,0 +1,23 @@ +getUri()->getExplodedPath(); + $model->setCollectionId(new CollectionId($path[2])); + } + } +} diff --git a/API/src/Handlers/Delete/Feed/Invitation/CommandHandler.php b/API/src/Handlers/Delete/Feed/Invitation/CommandHandler.php new file mode 100644 index 0000000..360c231 --- /dev/null +++ b/API/src/Handlers/Delete/Feed/Invitation/CommandHandler.php @@ -0,0 +1,35 @@ +deleteInvitationCommand = $deleteInvitationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $this->deleteInvitationCommand->execute($model->getFeedId(), $model->getUserId()); + + $model->setData([ + 'deleted' => true + ]); + } + } +} diff --git a/API/src/Handlers/Delete/Feed/Invitation/QueryHandler.php b/API/src/Handlers/Delete/Feed/Invitation/QueryHandler.php new file mode 100644 index 0000000..72debdb --- /dev/null +++ b/API/src/Handlers/Delete/Feed/Invitation/QueryHandler.php @@ -0,0 +1,55 @@ +accessControl = $accessControl; + $this->fetchInvitationQuery = $fetchInvitationQuery; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $feedId = $model->getFeedId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('invitation not found', 'not_found'); + } + + $invitation = $this->fetchInvitationQuery->execute($feedId, $model->getUserId()); + + if ($invitation === null) { + throw new NotFound('invitation not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + } + } +} diff --git a/API/src/Handlers/Delete/Feed/Invitation/RequestHandler.php b/API/src/Handlers/Delete/Feed/Invitation/RequestHandler.php new file mode 100644 index 0000000..0b742d3 --- /dev/null +++ b/API/src/Handlers/Delete/Feed/Invitation/RequestHandler.php @@ -0,0 +1,27 @@ +getUri()->getExplodedPath(); + + $model->setFeedId(new FeedId($parts[2])); + $model->setUserId($parts[4]); + } + } +} diff --git a/API/src/Handlers/Delete/Feed/People/CommandHandler.php b/API/src/Handlers/Delete/Feed/People/CommandHandler.php new file mode 100644 index 0000000..2a255d7 --- /dev/null +++ b/API/src/Handlers/Delete/Feed/People/CommandHandler.php @@ -0,0 +1,35 @@ +deleteFeedPersonCommand = $deleteFeedPersonCommand; + } + + public function execute(AbstractModel $model) + { + /** @var DeleteModel $model */ + + $this->deleteFeedPersonCommand->execute($model->getFeedId(), $model->getUserId()); + + $model->setData([ + 'deleted' => true + ]); + } + } +} diff --git a/API/src/Handlers/Delete/Feed/People/QueryHandler.php b/API/src/Handlers/Delete/Feed/People/QueryHandler.php new file mode 100644 index 0000000..d49ce72 --- /dev/null +++ b/API/src/Handlers/Delete/Feed/People/QueryHandler.php @@ -0,0 +1,61 @@ +fetchPersonQuery = $fetchPersonQuery; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var DeleteModel $model */ + + $feedId = $model->getFeedId(); + $userId = $model->getUserId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + + $person = $this->fetchPersonQuery->execute($feedId, $userId); + + if ($person === null) { + throw new NotFound('feed user not found', 'not_found'); + } + + if (!$this->accessControl->canModifyFeedUser($feedId, $userId, $accessToken)) { + throw new BadRequest('feed user cannot be deleted', 'delete_error'); + } + } + } +} diff --git a/API/src/Handlers/Delete/Feed/People/RequestHandler.php b/API/src/Handlers/Delete/Feed/People/RequestHandler.php new file mode 100644 index 0000000..fa753c1 --- /dev/null +++ b/API/src/Handlers/Delete/Feed/People/RequestHandler.php @@ -0,0 +1,26 @@ +getUri()->getExplodedPath(); + + $model->setFeedId(new FeedId($parts[2])); + $model->setUserId(new UserId($parts[4])); + } + } +} diff --git a/API/src/Handlers/Delete/Post/CommandHandler.php b/API/src/Handlers/Delete/Post/CommandHandler.php new file mode 100644 index 0000000..67cde26 --- /dev/null +++ b/API/src/Handlers/Delete/Post/CommandHandler.php @@ -0,0 +1,35 @@ +deletePostCommand = $deletePostCommand; + } + + public function execute(AbstractModel $model) + { + /** @var PostModel $model */ + + $this->deletePostCommand->execute($model->getPostId()); + + $model->setData([ + 'deleted' => 1 + ]); + } + } +} diff --git a/API/src/Handlers/Delete/Post/QueryHandler.php b/API/src/Handlers/Delete/Post/QueryHandler.php new file mode 100644 index 0000000..12c148a --- /dev/null +++ b/API/src/Handlers/Delete/Post/QueryHandler.php @@ -0,0 +1,45 @@ +fetchPostInfoQuery = $fetchPostInfoQuery; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var PostModel $model */ + + $post = $this->fetchPostInfoQuery->execute($model->getPostId()); + + // TODO: change access control + + if ($post === null || !$this->accessControl->hasPostAccess($post['feed_id'], $model->getAccessToken())) { + throw new NotFound('post not found', 'not_found'); + } + } + } +} diff --git a/API/src/Handlers/Delete/Post/RequestHandler.php b/API/src/Handlers/Delete/Post/RequestHandler.php new file mode 100644 index 0000000..6873043 --- /dev/null +++ b/API/src/Handlers/Delete/Post/RequestHandler.php @@ -0,0 +1,23 @@ +getUri()->getExplodedPath(); + + $model->setPostId($parts[2]); + } + } +} diff --git a/API/src/Handlers/Get/Collection/QueryHandler.php b/API/src/Handlers/Get/Collection/QueryHandler.php new file mode 100644 index 0000000..5a9a097 --- /dev/null +++ b/API/src/Handlers/Get/Collection/QueryHandler.php @@ -0,0 +1,56 @@ +fetchCollectionQuery = $fetchCollectionQuery; + $this->documentMapper = $documentMapper; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var CollectionModel $model */ + + $collection = $this->fetchCollectionQuery->execute($model->getCollectionId()); + + if ($collection === null || !$this->accessControl->hasReadAccess($model->getAccessToken(), $collection)) { + throw new NotFound('collection not found', 'not_found'); + } + + $model->setData($this->documentMapper->map($collection)); + } + } +} diff --git a/API/src/Handlers/Get/Collection/RequestHandler.php b/API/src/Handlers/Get/Collection/RequestHandler.php new file mode 100644 index 0000000..40f15dc --- /dev/null +++ b/API/src/Handlers/Get/Collection/RequestHandler.php @@ -0,0 +1,23 @@ +getUri()->getExplodedPath(); + $model->setCollectionId(new CollectionId($path[2])); + } + } +} diff --git a/API/src/Handlers/Get/Feed/Invitations/QueryHandler.php b/API/src/Handlers/Get/Feed/Invitations/QueryHandler.php new file mode 100644 index 0000000..5519f0c --- /dev/null +++ b/API/src/Handlers/Get/Feed/Invitations/QueryHandler.php @@ -0,0 +1,64 @@ +accessControl = $accessControl; + $this->fetchInvitationsQuery = $fetchInvitationsQuery; + $this->feedUserMapper = $feedUserMapper; + } + + public function execute(AbstractModel $model) + { + /** @var FeedModel $model */ + + $feedId = $model->getFeedId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + + $invitations = $this->fetchInvitationsQuery->execute($feedId); + + foreach ($invitations as $i => $invitation) { + $invitations[$i] = $this->feedUserMapper->map($invitation); + } + + $model->setData($invitations); + } + } +} diff --git a/API/src/Handlers/Get/Feed/People/QueryHandler.php b/API/src/Handlers/Get/Feed/People/QueryHandler.php new file mode 100644 index 0000000..d500ace --- /dev/null +++ b/API/src/Handlers/Get/Feed/People/QueryHandler.php @@ -0,0 +1,68 @@ +fetchPeopleQuery = $fetchPeopleQuery; + $this->accessControl = $accessControl; + $this->feedUserMapper = $feedUserMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $feedId = $model->getFeedId(); + $accessToken = null; + + if ($model->hasAccessToken()) { + $accessToken = $model->getAccessToken(); + } + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + $users = $this->fetchPeopleQuery->execute($feedId); + + foreach ($users as $i => $user) { + $user['meta']['is_modifiable'] = $this->accessControl->canModifyFeedUser($feedId, $user['user_id'], $accessToken); + $users[$i] = $this->feedUserMapper->map($user); + } + + $model->setData($users); + } + } +} diff --git a/API/src/Handlers/Get/Feed/People/RequestHandler.php b/API/src/Handlers/Get/Feed/People/RequestHandler.php new file mode 100644 index 0000000..540d595 --- /dev/null +++ b/API/src/Handlers/Get/Feed/People/RequestHandler.php @@ -0,0 +1,23 @@ +getUri()->getExplodedPath(); + + $model->setFeedId($parts[2]); + } + } +} diff --git a/API/src/Handlers/Get/Feed/Posts/QueryHandler.php b/API/src/Handlers/Get/Feed/Posts/QueryHandler.php new file mode 100644 index 0000000..1ba6aef --- /dev/null +++ b/API/src/Handlers/Get/Feed/Posts/QueryHandler.php @@ -0,0 +1,77 @@ +fetchFeedPostsQuery = $fetchFeedPostsQuery; + $this->accessControl = $accessControl; + $this->resultsMapper = $resultsMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $feedId = $model->getFeedId(); + $limit = $model->getLimit(); + $page = $model->getPage(); + + $accessToken = null; + $userId = null; + + if ($model->hasAccessToken()) { + $accessToken = $model->getAccessToken(); + } + + if ($model->hasAuthUserId()) { + $userId = $model->getAuthUserId(); + } + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + $posts = $this->fetchFeedPostsQuery->execute($feedId, $limit, $page, $userId); + + $model->setData(new Pagination( + $model->getLimit(), + $model->getPage(), + $posts['hits']['total'], + $this->resultsMapper->map($posts) + )); + } + } +} diff --git a/API/src/Handlers/Get/Feed/Posts/RequestHandler.php b/API/src/Handlers/Get/Feed/Posts/RequestHandler.php new file mode 100644 index 0000000..3249de0 --- /dev/null +++ b/API/src/Handlers/Get/Feed/Posts/RequestHandler.php @@ -0,0 +1,26 @@ +getUri()->getExplodedPath(); + $feedId = new FeedId($parts[2]); + + $model->setFeedId($feedId); + } + } +} diff --git a/API/src/Handlers/Get/Feed/QueryHandler.php b/API/src/Handlers/Get/Feed/QueryHandler.php new file mode 100644 index 0000000..b9dd6d7 --- /dev/null +++ b/API/src/Handlers/Get/Feed/QueryHandler.php @@ -0,0 +1,105 @@ +fetchFeedQuery = $fetchFeedQuery; + $this->fetchFeedVanityQuery = $fetchFeedVanityQuery; + $this->invitationExistsQuery = $invitationExistsQuery; + $this->feedMapper = $feedMapper; + $this->accessCheck = $accessCheck; + } + + public function execute(AbstractModel $model) + { + /** @var FeedModel $model */ + + $feedId = $model->getFeedId(); + $userId = null; + $token = null; + + if ($model->hasAuthUserId()) { + $userId = $model->getAuthUserId(); + } + + if ($model->hasAccessToken()) { + $token = $model->getAccessToken(); + } + + $isInvited = false; + + if ($userId !== null) { + $isInvited = $this->invitationExistsQuery->execute($feedId, $userId); + } + + $hasReadAccess = ($this->accessCheck->hasReadAccess($feedId, $token) || $isInvited); + + if (!$hasReadAccess) { + throw new NotFound('feed not found', 'not_found'); + } + + $feed = $this->fetchFeedQuery->execute($feedId, $userId); + $hasWriteAccess = $this->accessCheck->hasWriteAccess($feedId, $token); + + $feed['vanity'] = $this->fetchFeedVanityQuery->execute($feedId); + $feed['access']['post'] = $this->accessCheck->hasPostAccess($feedId, $token); + $feed['access']['manage_users'] = $hasWriteAccess; + + if ($userId !== null) { + $feed['user']['invited'] = $isInvited; + } + + $model->setData($this->feedMapper->map($feed)); + } + } +} diff --git a/API/src/Handlers/Get/Feed/RequestHandler.php b/API/src/Handlers/Get/Feed/RequestHandler.php new file mode 100644 index 0000000..47c4432 --- /dev/null +++ b/API/src/Handlers/Get/Feed/RequestHandler.php @@ -0,0 +1,25 @@ +getUri()->getExplodedPath(); + $feedId = new FeedId($path[2]); + + $model->setFeedId($feedId); + } + } +} diff --git a/API/src/Handlers/Get/Feeds/QueryHandler.php b/API/src/Handlers/Get/Feeds/QueryHandler.php new file mode 100644 index 0000000..d0f582b --- /dev/null +++ b/API/src/Handlers/Get/Feeds/QueryHandler.php @@ -0,0 +1,53 @@ +fetchFeedsQuery = $fetchFeedsQuery; + $this->feedMapper = $feedMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + + $feeds = $this->fetchFeedsQuery->execute($limit, $page); + + foreach ($feeds as $i => $feed) { + $feeds[$i] = $this->feedMapper->map($feed); + } + + $model->setData([ + 'pagination' => [ + 'limit' => $limit, + 'page' => $page + ], + 'feeds' => $feeds + ]); + } + } +} diff --git a/API/src/Handlers/Get/Index/QueryHandler.php b/API/src/Handlers/Get/Index/QueryHandler.php new file mode 100644 index 0000000..fb21ea7 --- /dev/null +++ b/API/src/Handlers/Get/Index/QueryHandler.php @@ -0,0 +1,21 @@ +setData([ + 'message' => 'welcome to the timetabio api' + ]); + } + } +} diff --git a/API/src/Handlers/Get/ListRequestHandler.php b/API/src/Handlers/Get/ListRequestHandler.php new file mode 100644 index 0000000..0a0ad22 --- /dev/null +++ b/API/src/Handlers/Get/ListRequestHandler.php @@ -0,0 +1,46 @@ +setLimit($request, $model); + $this->setPage($request, $model); + } + + private function setLimit(RequestInterface $request, ListModel $model) + { + try { + $model->setLimit($request->getQueryParam('limit')); + } catch (\Throwable $exception) { + } + + if ($model->getLimit() < 1) { + throw new BadRequest('limit must be above zero', 'invalid_limit'); + } + } + + private function setPage(RequestInterface $request, ListModel $model) + { + try { + $model->setPage($request->getQueryParam('page')); + } catch (\Throwable $exception) { + } + + if ($model->getPage() < 0) { + throw new BadRequest('page must be a positive integer', 'invalid_page'); + } + } + } +} diff --git a/API/src/Handlers/Get/Post/QueryHandler.php b/API/src/Handlers/Get/Post/QueryHandler.php new file mode 100644 index 0000000..cc8db87 --- /dev/null +++ b/API/src/Handlers/Get/Post/QueryHandler.php @@ -0,0 +1,86 @@ +fetchPostQuery = $fetchPostQuery; + $this->fetchPostAttachmentsQuery = $fetchPostAttachmentsQuery; + $this->postMapper = $postMapper; + $this->postAttachmentMapper = $postAttachmentMapper; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var PostModel $model */ + + $userId = null; + $token = null; + + if ($model->hasAuthUserId()) { + $userId = $model->getAuthUserId(); + } + + if ($model->hasAccessToken()) { + $token = $model->getAccessToken(); + } + + $post = $this->fetchPostQuery->execute($model->getPostId(), $userId); + + if ($post === null || !$this->accessControl->hasReadAccess($post['feed_id'], $token)) { + throw new NotFound('post not found', 'not_found'); + } + + $attachments = $this->fetchPostAttachmentsQuery->execute($model->getPostId()); + + foreach ($attachments as $i => $attachment) { + $attachments[$i] = $this->postAttachmentMapper->map($attachment); + } + + $post['attachments'] = $attachments; + $post['feed']['access']['post'] = $this->accessControl->hasPostAccess($post['feed_id'], $token); + + $model->setData($this->postMapper->map($post)); + } + } +} diff --git a/API/src/Handlers/Get/Post/RequestHandler.php b/API/src/Handlers/Get/Post/RequestHandler.php new file mode 100644 index 0000000..f30c912 --- /dev/null +++ b/API/src/Handlers/Get/Post/RequestHandler.php @@ -0,0 +1,23 @@ +getUri()->getExplodedPath(); + + $model->setPostId($parts[2]); + } + } +} diff --git a/API/src/Handlers/Get/Profile/QueryHandler.php b/API/src/Handlers/Get/Profile/QueryHandler.php new file mode 100644 index 0000000..08df691 --- /dev/null +++ b/API/src/Handlers/Get/Profile/QueryHandler.php @@ -0,0 +1,45 @@ +fetchProfileQuery = $fetchProfileQuery; + $this->documentMapper = $documentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ProfileModel $model */ + + $profile = $this->fetchProfileQuery->execute($model->getUsername()); + + if ($profile === null) { + throw new NotFound('profile not found', 'not_found'); + } + + $model->setData($this->documentMapper->map($profile)); + } + } +} diff --git a/API/src/Handlers/Get/Profile/RequestHandler.php b/API/src/Handlers/Get/Profile/RequestHandler.php new file mode 100644 index 0000000..8fc7ae9 --- /dev/null +++ b/API/src/Handlers/Get/Profile/RequestHandler.php @@ -0,0 +1,31 @@ +getUri()->getExplodedPath(); + + try { + $username = new Username($path[2]); + } catch (\Exception $exception) { + throw new BadRequest('invalid username', 'invalid_username'); + } + + $model->setUsername($username); + } + } +} diff --git a/API/src/Handlers/Get/Random/QueryHandler.php b/API/src/Handlers/Get/Random/QueryHandler.php new file mode 100644 index 0000000..2c2a2ab --- /dev/null +++ b/API/src/Handlers/Get/Random/QueryHandler.php @@ -0,0 +1,31 @@ +setData([ + 'number' => $this->getRandomNumber(), + ]); + } + + /** + * @see https://xkcd.com/221/ + */ + private function getRandomNumber(): int + { + return 4; // chosen by fair dice roll. + // guaranteed to be random. + } + } +} diff --git a/API/src/Handlers/Get/Search/QueryHandler.php b/API/src/Handlers/Get/Search/QueryHandler.php new file mode 100644 index 0000000..97d7630 --- /dev/null +++ b/API/src/Handlers/Get/Search/QueryHandler.php @@ -0,0 +1,55 @@ +searchQuery = $searchQuery; + $this->searchResultsMapper = $searchResultsMapper; + } + + public function execute(AbstractModel $model) + { + /** @var SearchModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + + $results = $this->searchQuery->execute( + $model->getQuery(), + $model->getType(), + $model->getAuthUserId(), + $limit, + $page + ); + + $model->setData(new Pagination( + $limit, + $page, + $results['hits']['total'], + $this->searchResultsMapper->map($results) + )); + } + } +} diff --git a/API/src/Handlers/Get/Search/RequestHandler.php b/API/src/Handlers/Get/Search/RequestHandler.php new file mode 100644 index 0000000..7095572 --- /dev/null +++ b/API/src/Handlers/Get/Search/RequestHandler.php @@ -0,0 +1,44 @@ +searchTypeLocator = $searchTypeLocator; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var SearchModel $model */ + + parent::execute($request, $model); + + if (!$request->hasQueryParam('query')) { + throw new BadRequest('missing parameter \'query\'', 'missing_parameter'); + } + + $model->setQuery($request->getQueryParam('query')); + + if ($request->hasQueryParam('type')) { + $type = $this->searchTypeLocator->locate($request->getQueryParam('type')); + $model->setType($type); + } + } + } +} diff --git a/API/src/Handlers/Get/User/Collections/QueryHandler.php b/API/src/Handlers/Get/User/Collections/QueryHandler.php new file mode 100644 index 0000000..b2eb73d --- /dev/null +++ b/API/src/Handlers/Get/User/Collections/QueryHandler.php @@ -0,0 +1,61 @@ +fetchUserCollectionsQuery = $fetchUserCollectionQuery; + $this->documentMapper = $documentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + + $collections = $this->fetchUserCollectionsQuery->execute($model->getAuthUserId(), $limit, $page); + + $mapped = []; + + foreach ($collections as $collection) { + $mapped[] = $this->documentMapper->map($collection); + } + + $model->setData([ + 'pagination' => [ + 'limit' => $limit, + 'page' => $page + ], + 'collections' => $mapped + ]); + + return $mapped; + + } + } +} diff --git a/API/src/Handlers/Get/User/Feed/QueryHandler.php b/API/src/Handlers/Get/User/Feed/QueryHandler.php new file mode 100644 index 0000000..6bdaa30 --- /dev/null +++ b/API/src/Handlers/Get/User/Feed/QueryHandler.php @@ -0,0 +1,55 @@ +fetchUserFeedQuery = $fetchUserFeedQuery; + $this->resultsMapper = $resultsMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + + $posts = $this->fetchUserFeedQuery->execute( + $model->getAuthUserId(), + $limit, + $page + ); + + $mapped = $this->resultsMapper->map($posts); + + $model->setData(new Pagination( + $limit, + $page, + $posts['hits']['total'], + $mapped + )); + } + } +} diff --git a/API/src/Handlers/Get/User/Feeds/QueryHandler.php b/API/src/Handlers/Get/User/Feeds/QueryHandler.php new file mode 100644 index 0000000..ed93dd1 --- /dev/null +++ b/API/src/Handlers/Get/User/Feeds/QueryHandler.php @@ -0,0 +1,50 @@ +fetchUserFeedsQuery = $fetchUserFeedsQuery; + $this->resultsMapper = $resultsMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + $userId = $model->getAuthUserId(); + + $feeds = $this->fetchUserFeedsQuery->execute($userId, $limit, $page); + + $model->setData(new Pagination( + $limit, + $page, + $feeds['hits']['total'], + $this->resultsMapper->map($feeds) + )); + } + } +} diff --git a/API/src/Handlers/Get/User/QueryHandler.php b/API/src/Handlers/Get/User/QueryHandler.php new file mode 100644 index 0000000..ef268b1 --- /dev/null +++ b/API/src/Handlers/Get/User/QueryHandler.php @@ -0,0 +1,44 @@ +fetchUserByIdQuery = $fetchUserByIdQuery; + $this->userMapper = $userMapper; + } + + public function execute(AbstractModel $model) + { + /** @var APIModel $model */ + + $data = $this->fetchUserByIdQuery->execute( + $model->getAuthUserId() + ); + + $user = $this->userMapper->map($data); + + $model->setData($user); + } + } +} diff --git a/API/src/Handlers/Get/User/Todo/QueryHandler.php b/API/src/Handlers/Get/User/Todo/QueryHandler.php new file mode 100644 index 0000000..865bfac --- /dev/null +++ b/API/src/Handlers/Get/User/Todo/QueryHandler.php @@ -0,0 +1,54 @@ +fetchTodoTasksQuery = $fetchTodoTasksQuery; + $this->postMapper = $postMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + $userId = $model->getAuthUserId(); + + $posts = $this->fetchTodoTasksQuery->execute($userId, $limit, $page); + + foreach ($posts as $i => $post) { + $posts[$i] = $this->postMapper->map($post); + } + + $model->setData([ + 'pagination' => [ + 'limit' => $limit, + 'page' => $page + ], + 'posts' => $posts + ]); + } + } +} diff --git a/API/src/Handlers/Get/User/Upcoming/QueryHandler.php b/API/src/Handlers/Get/User/Upcoming/QueryHandler.php new file mode 100644 index 0000000..d16b51f --- /dev/null +++ b/API/src/Handlers/Get/User/Upcoming/QueryHandler.php @@ -0,0 +1,54 @@ +fetchUpcomingEventsQuery = $fetchUpcomingEventsQuery; + $this->postMapper = $postMapper; + } + + public function execute(AbstractModel $model) + { + /** @var ListModel $model */ + + $limit = $model->getLimit(); + $page = $model->getPage(); + $userId = $model->getAuthUserId(); + + $posts = $this->fetchUpcomingEventsQuery->execute($userId, $limit, $page); + + foreach ($posts as $i => $post) { + $posts[$i] = $this->postMapper->map($post); + } + + $model->setData([ + 'pagination' => [ + 'limit' => $limit, + 'page' => $page + ], + 'posts' => $posts + ]); + } + } +} diff --git a/API/src/Handlers/Patch/Collection/CommandHandler.php b/API/src/Handlers/Patch/Collection/CommandHandler.php new file mode 100644 index 0000000..fb3417f --- /dev/null +++ b/API/src/Handlers/Patch/Collection/CommandHandler.php @@ -0,0 +1,43 @@ +updateCollectionCommand = $updateCollectionCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $updates = $model->getUpdates(); + + if (empty($updates)) { + throw new BadRequest('no fields given to update', 'no_update'); + } + + $this->updateCollectionCommand->execute($model->getCollectionId(), $updates); + + $updates['id'] = (string) $model->getCollectionId(); + + $model->setData($updates); + } + } +} diff --git a/API/src/Handlers/Patch/Collection/QueryHandler.php b/API/src/Handlers/Patch/Collection/QueryHandler.php new file mode 100644 index 0000000..941dec5 --- /dev/null +++ b/API/src/Handlers/Patch/Collection/QueryHandler.php @@ -0,0 +1,46 @@ +fetchCollectionQuery = $fetchCollectionQuery; + $this->accessControl = $collectionAccessControl; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $token = $model->getAccessToken(); + + $collection = $this->fetchCollectionQuery->execute($model->getCollectionId()); + + if ($collection === null || !$this->accessControl->hasWriteAccess($token, $collection)) { + throw new NotFound('collection not found', 'not_found'); + } + } + } +} diff --git a/API/src/Handlers/Patch/Collection/RequestHandler.php b/API/src/Handlers/Patch/Collection/RequestHandler.php new file mode 100644 index 0000000..a4b1271 --- /dev/null +++ b/API/src/Handlers/Patch/Collection/RequestHandler.php @@ -0,0 +1,30 @@ +getUri()->getExplodedPath(); + + $model->setCollectionId(new CollectionId($parts[2])); + + if ($request->hasParam('name')) { + $model->addUpdate('name', $request->getParam('name')); + } + } + } +} diff --git a/API/src/Handlers/Patch/Feed/CommandHandler.php b/API/src/Handlers/Patch/Feed/CommandHandler.php new file mode 100644 index 0000000..8ca1cc0 --- /dev/null +++ b/API/src/Handlers/Patch/Feed/CommandHandler.php @@ -0,0 +1,57 @@ +updateFeedCommand = $updateFeedCommand; + $this->setFeedVanityCommand = $setFeedVanityCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $feedId = $model->getFeedId(); + $updates = $model->getUpdates(); + + if (!empty($updates)) { + $this->updateFeedCommand->execute($feedId, $updates); + } + + if ($model->hasFeedVanity()) { + $vanity = (string) $model->getFeedVanity(); + $updates['vanity'] = $vanity; + + $this->setFeedVanityCommand->execute($feedId, $vanity); + } + + $updates['id'] = (string) $feedId; + + $model->setData($updates); + } + } +} diff --git a/API/src/Handlers/Patch/Feed/Invitation/CommandHandler.php b/API/src/Handlers/Patch/Feed/Invitation/CommandHandler.php new file mode 100644 index 0000000..0ec697a --- /dev/null +++ b/API/src/Handlers/Patch/Feed/Invitation/CommandHandler.php @@ -0,0 +1,40 @@ +updateInvitationCommand = $updateInvitationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $this->updateInvitationCommand->execute( + $model->getFeedId(), + $model->getUserId(), + $model->getRole() + ); + + $model->setData([ + 'user_id' => $model->getUserId(), + 'role' => (string) $model->getRole() + ]); + } + } +} diff --git a/API/src/Handlers/Patch/Feed/Invitation/QueryHandler.php b/API/src/Handlers/Patch/Feed/Invitation/QueryHandler.php new file mode 100644 index 0000000..10d14aa --- /dev/null +++ b/API/src/Handlers/Patch/Feed/Invitation/QueryHandler.php @@ -0,0 +1,55 @@ +accessControl = $accessControl; + $this->fetchInvitationQuery = $fetchInvitationQuery; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $feedId = $model->getFeedId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('invitation not found', 'not_found'); + } + + $invitation = $this->fetchInvitationQuery->execute($feedId, $model->getUserId()); + + if ($invitation === null) { + throw new NotFound('invitation not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + } + } +} diff --git a/API/src/Handlers/Patch/Feed/Invitation/RequestHandler.php b/API/src/Handlers/Patch/Feed/Invitation/RequestHandler.php new file mode 100644 index 0000000..e978658 --- /dev/null +++ b/API/src/Handlers/Patch/Feed/Invitation/RequestHandler.php @@ -0,0 +1,11 @@ +fetchVanityByNameQuery = $fetchVanityByNameQuery; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $token = $model->getAccessToken(); + $feedId = $model->getFeedId(); + + if (!$this->accessControl->hasReadAccess($feedId, $token)) { + throw new NotFound('feed not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $token)) { + throw new Forbidden('access denied', 'access_denied'); + } + + if ($model->hasFeedVanity()) { + $this->checkVanity($model); + } + } + + private function checkVanity(UpdateModel $model) + { + $vanity = $this->fetchVanityByNameQuery->execute($model->getFeedVanity()); + + if ($vanity === null || $vanity['feed_id'] === (string) $model->getFeedId()) { + return; + } + + throw new BadRequest('vanity already taken', 'vanity_taken'); + } + } +} diff --git a/API/src/Handlers/Patch/Feed/RequestHandler.php b/API/src/Handlers/Patch/Feed/RequestHandler.php new file mode 100644 index 0000000..0dbc097 --- /dev/null +++ b/API/src/Handlers/Patch/Feed/RequestHandler.php @@ -0,0 +1,92 @@ +getUri()->getExplodedPath(); + $feedId = new FeedId($parts[2]); + + $model->setFeedId($feedId); + + if ($request->hasParam('name')) { + $model->addUpdate('name', $this->getName($request)); + } + + if ($request->hasParam('description')) { + $model->addUpdate('description', $this->getDescription($request)); + } + + if ($request->hasParam('is_private')) { + $model->addUpdate('is_private', $this->getIsPrivate($request)); + } + + if ($request->hasParam('vanity')) { + $model->setFeedVanity($this->getVanity($request)); + } + } + + private function getVanity(PatchRequest $request): string + { + $vanity = $request->getParam('vanity'); + + if ($vanity === '') { + return $vanity; + } + + try { + return new FeedVanity($vanity); + } catch (\Exception $exception) { + throw new BadRequest('invalid vanity name', 'invalid_vanity', $exception); + } + } + + private function getIsPrivate(PatchRequest $request): bool + { + try { + $raw = $request->getParam('is_private'); + + return (new StringBoolean($raw))->getValue(); + } catch (\Exception $exception) { + throw new BadRequest('invalid boolean value must be 1 or 0', 'invalid_is_private', $exception); + } + } + + private function getName(PatchRequest $request): FeedName + { + try { + return new FeedName($request->getParam('name')); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_feed_name', $exception); + } + } + + private function getDescription(PatchRequest $request): FeedDescription + { + try { + return new FeedDescription($request->getParam('description')); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_description', $exception); + } + } + } +} diff --git a/API/src/Handlers/Patch/Feed/User/CommandHandler.php b/API/src/Handlers/Patch/Feed/User/CommandHandler.php new file mode 100644 index 0000000..ea9458f --- /dev/null +++ b/API/src/Handlers/Patch/Feed/User/CommandHandler.php @@ -0,0 +1,40 @@ +updateFeedUserCommand = $updateFeedUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $this->updateFeedUserCommand->execute( + $model->getFeedId(), + $model->getUserId(), + $model->getRole() + ); + + $model->setData([ + 'user_id' => $model->getUserId(), + 'role' => (string) $model->getRole() + ]); + } + } +} diff --git a/API/src/Handlers/Patch/Feed/User/QueryHandler.php b/API/src/Handlers/Patch/Feed/User/QueryHandler.php new file mode 100644 index 0000000..15e8a12 --- /dev/null +++ b/API/src/Handlers/Patch/Feed/User/QueryHandler.php @@ -0,0 +1,61 @@ +accessControl = $accessControl; + $this->fetchFeedUserQuery = $fetchFeedUserQuery; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateModel $model */ + + $feedId = $model->getFeedId(); + $userId = $model->getUserId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed user not found', 'not_found'); + } + + $user = $this->fetchFeedUserQuery->execute($feedId, $userId); + + if ($user === null) { + throw new NotFound('feed user not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + + if (!$this->accessControl->canModifyFeedUser($feedId, $userId, $accessToken)) { + throw new BadRequest('feed user cannot be updated', 'update_error'); + } + } + } +} diff --git a/API/src/Handlers/Patch/Feed/User/RequestHandler.php b/API/src/Handlers/Patch/Feed/User/RequestHandler.php new file mode 100644 index 0000000..f041764 --- /dev/null +++ b/API/src/Handlers/Patch/Feed/User/RequestHandler.php @@ -0,0 +1,50 @@ +userRoleLocator = $userRoleLocator; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var PostRequest $request */ + /** @var UpdateModel $model */ + + $parts = $request->getUri()->getExplodedPath(); + + if (!$request->hasParam('role')) { + throw new BadRequest('missing parameter \'role\'', 'missing_parameter'); + } + + try { + $role = $this->userRoleLocator->locate($request->getParam('role')); + } catch (\Exception $exception) { + throw new BadRequest('invalid user role', 'invalid_role'); + } + + $model->setFeedId(new FeedId($parts[2])); + $model->setUserId($parts[4]); + $model->setRole($role); + } + } +} diff --git a/API/src/Handlers/Patch/User/CommandHandler.php b/API/src/Handlers/Patch/User/CommandHandler.php new file mode 100644 index 0000000..bbbe7f7 --- /dev/null +++ b/API/src/Handlers/Patch/User/CommandHandler.php @@ -0,0 +1,42 @@ +updateUserCommand = $updateUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateUserModel $model */ + + $updates = $model->getUpdates(); + + if (empty($updates)) { + throw new BadRequest('no fields given to update', 'no_update'); + } + + $this->updateUserCommand->execute($model->getAuthUserId(), $updates); + + $updates['id'] = (string) $model->getAuthUserId(); + + $model->setData($updates); + } + } +} diff --git a/API/src/Handlers/Patch/User/QueryHandler.php b/API/src/Handlers/Patch/User/QueryHandler.php new file mode 100644 index 0000000..f73830e --- /dev/null +++ b/API/src/Handlers/Patch/User/QueryHandler.php @@ -0,0 +1,41 @@ +fetchUsernameQuery = $fetchUsernameQuery; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateUserModel $model */ + + if (!$model->hasUpdate('username')) { + return; + } + + $currentUsername = $this->fetchUsernameQuery->execute($model->getAuthUserId()); + $newUsername = $model->getUpdate('username'); + + if (mb_strtolower($newUsername) !== mb_strtolower($currentUsername)) { + throw new BadRequest('only case changes allowed for username', 'invalid_username'); + } + } + } +} diff --git a/API/src/Handlers/Patch/User/RequestHandler.php b/API/src/Handlers/Patch/User/RequestHandler.php new file mode 100644 index 0000000..a39a159 --- /dev/null +++ b/API/src/Handlers/Patch/User/RequestHandler.php @@ -0,0 +1,29 @@ +hasParam('name')) { + $model->addUpdate('name', trim($request->getParam('name'))); + } + + if ($request->hasParam('username')) { + $model->addUpdate('username', $request->getParam('username')); + } + } + } +} diff --git a/API/src/Handlers/Post/Auth/CommandHandler.php b/API/src/Handlers/Post/Auth/CommandHandler.php new file mode 100644 index 0000000..248e961 --- /dev/null +++ b/API/src/Handlers/Post/Auth/CommandHandler.php @@ -0,0 +1,65 @@ +saveAccessTokenCommand = $saveAccessTokenCommand; + } + + public function execute(AbstractModel $model) + { + /** @var AuthModel $model */ + + if ($model->getAutoRenew() && !$this->checkAutoRenewAccess($model)) { + throw new Forbidden('cannot use auto_renew', 'access_denied'); + } + + $token = new Token; + + $accessToken = new AccessToken( + $token, + $model->getAccessType(), + $model->getAuthUserId(), + $model->getAutoRenew() + ); + + $this->saveAccessTokenCommand->execute($accessToken); + + $model->setData([ + 'access_token' => (string) $token, + 'user_id' => (string) $accessToken->getUserId(), + 'expires' => $accessToken->getExpires(), + 'auto_renew' => $accessToken->getAutoRenew(), + 'scopes' => $model->getAccessType() + ]); + } + + private function checkAutoRenewAccess(AuthModel $model) + { + if (!$model->hasAccessToken()) { + return false; + } + + return $model->getAccessToken()->getAccessType() instanceof SystemAccess; + } + } +} diff --git a/API/src/Handlers/Post/Auth/QueryHandler.php b/API/src/Handlers/Post/Auth/QueryHandler.php new file mode 100644 index 0000000..bf48b63 --- /dev/null +++ b/API/src/Handlers/Post/Auth/QueryHandler.php @@ -0,0 +1,47 @@ +fetchAuthUserQuery = $fetchAuthUserQuery; + } + + public function execute(AbstractModel $model) + { + /** @var AuthModel $model */ + + $user = $this->fetchAuthUserQuery->execute($model->getUser()); + + if ($user === null) { + throw new BadRequest('invalid login', 'invalid_login'); + } + + if (!password_verify($model->getPassword(), $user['password'])) { + throw new BadRequest('invalid login', 'invalid_login'); + } + + if (!$user['is_verified']) { + throw new BadRequest('user is not verified', 'user_not_verified'); + } + + $model->setAuthUserId(new UserId($user['id'])); + } + } +} diff --git a/API/src/Handlers/Post/Auth/RequestHandler.php b/API/src/Handlers/Post/Auth/RequestHandler.php new file mode 100644 index 0000000..d0391e9 --- /dev/null +++ b/API/src/Handlers/Post/Auth/RequestHandler.php @@ -0,0 +1,69 @@ +hasParam('user')) { + throw new BadRequest('required parameter \'user\' is missing', 'parameter_missing'); + } + + if (!$request->hasParam('password')) { + throw new BadRequest('required parameter \'password\' is missing', 'parameter_missing'); + } + + $user = $request->getParam('user'); + $password = $request->getParam('password'); + $scopes = '*'; + $autoRenew = false; + + if ($request->hasParam('scopes')) { + $scopes = $request->getParam('scopes'); + } + + if ($request->hasParam('auto_renew')) { + try { + $autoRenew = (new StringBoolean($request->getParam('auto_renew')))->getValue(); + } catch (\Exception $exception) { + throw new BadRequest('auto_renew must either be true or false', 'invalid_auto_renew'); + } + } + + try { + $accessType = $this->getAccessType($scopes); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_scope'); + } + + $model->setUser($user); + $model->setPassword($password); + $model->setAccessType($accessType); + $model->setAutoRenew($autoRenew); + } + + private function getAccessType(string $scopes): AccessTypeInterface + { + if ($scopes === '*') { + return new \Timetabio\API\Access\AccessTypes\FullAccess; + } + + return new \Timetabio\API\Access\AccessTypes\ScopedAccess(explode(',', $scopes)); + } + } +} diff --git a/API/src/Handlers/Post/BetaRequest/CommandHandler.php b/API/src/Handlers/Post/BetaRequest/CommandHandler.php new file mode 100644 index 0000000..6229678 --- /dev/null +++ b/API/src/Handlers/Post/BetaRequest/CommandHandler.php @@ -0,0 +1,33 @@ +createBetaRequestCommand = $createBetaRequestCommand; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $model->setData( + $this->createBetaRequestCommand->execute($model->getEmail()) + ); + } + } +} diff --git a/API/src/Handlers/Post/BetaRequest/QueryHandler.php b/API/src/Handlers/Post/BetaRequest/QueryHandler.php new file mode 100644 index 0000000..a7f2eea --- /dev/null +++ b/API/src/Handlers/Post/BetaRequest/QueryHandler.php @@ -0,0 +1,34 @@ +fetchBetaRequestByEmailQuery = $fetchBetaRequestByEmailQuery; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + if ($this->fetchBetaRequestByEmailQuery->execute($model->getEmail())) { + throw new BadRequest('email already added', 'email_already_added'); + } + } + } +} diff --git a/API/src/Handlers/Post/BetaRequest/RequestHandler.php b/API/src/Handlers/Post/BetaRequest/RequestHandler.php new file mode 100644 index 0000000..5dbac76 --- /dev/null +++ b/API/src/Handlers/Post/BetaRequest/RequestHandler.php @@ -0,0 +1,33 @@ +hasParam('email')) { + throw new BadRequest('missing parameter \'email\'', 'missing_parameter'); + } + + try { + $model->setEmail(new EmailAddress($request->getParam('email'))); + } catch (\Exception $exception) { + throw new BadRequest('invalid email address', 'invalid_email'); + } + } + } +} diff --git a/API/src/Handlers/Post/Collections/CommandHandler.php b/API/src/Handlers/Post/Collections/CommandHandler.php new file mode 100644 index 0000000..8e09016 --- /dev/null +++ b/API/src/Handlers/Post/Collections/CommandHandler.php @@ -0,0 +1,46 @@ +createCollectionCommand = $createCollectionCommand; + $this->documentMapper = $documentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $collection = $this->createCollectionCommand->execute( + $model->getCollectionName(), + $model->getAuthUserId() + ); + + $model->setData($this->documentMapper->map($collection)); + } + } +} diff --git a/API/src/Handlers/Post/Collections/CreateCollectionCommandHandler.php b/API/src/Handlers/Post/Collections/CreateCollectionCommandHandler.php new file mode 100644 index 0000000..1d81455 --- /dev/null +++ b/API/src/Handlers/Post/Collections/CreateCollectionCommandHandler.php @@ -0,0 +1,55 @@ +createCollectionCommand = $createCollectionCommand; + $this->getDocumentMapper = $getDocumentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var APIModel $model */ + /** @var PostRequest $request */ + + try { + $collectionName = new CollectionName($request->getParam('name')); + } catch (\Exception $e) { + throw new BadRequest('invalid name', 'invalid_name'); + } + + $userId = $model->getUserId(); + $collection = $this->createCollectionCommand->execute($collectionName, $userId); + $mappedCollection = $this->getDocumentMapper->map($collection); + + $model->setData($mappedCollection); + } + } +} diff --git a/API/src/Handlers/Post/Collections/RequestHandler.php b/API/src/Handlers/Post/Collections/RequestHandler.php new file mode 100644 index 0000000..41df26f --- /dev/null +++ b/API/src/Handlers/Post/Collections/RequestHandler.php @@ -0,0 +1,35 @@ +hasParam('name')) { + throw new BadRequest('missing parameter \'name\'', 'missing_parameter'); + } + + try { + $name = new CollectionName($request->getParam('name')); + } catch (\Exception $e) { + throw new BadRequest('invalid name', 'invalid_name'); + } + + $model->setCollectionName($name); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Follow/CommandHandler.php b/API/src/Handlers/Post/Feed/Follow/CommandHandler.php new file mode 100644 index 0000000..83bd53f --- /dev/null +++ b/API/src/Handlers/Post/Feed/Follow/CommandHandler.php @@ -0,0 +1,49 @@ +followFeedCommand = $followFeedCommand; + $this->deleteInvitationCommand = $deleteInvitationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + $feedId = $model->getFeedId(); + $authUserId = $model->getAuthUserId(); + + if (!$model->isFollowing()) { + $this->followFeedCommand->execute($feedId, $authUserId, $model->getRole()); + $this->deleteInvitationCommand->execute($feedId, $authUserId); + } + + $model->setData([ + 'feed_id' => $model->getFeedId(), + 'role' => $model->getRole() + ]); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Follow/QueryHandler.php b/API/src/Handlers/Post/Feed/Follow/QueryHandler.php new file mode 100644 index 0000000..4e8cee0 --- /dev/null +++ b/API/src/Handlers/Post/Feed/Follow/QueryHandler.php @@ -0,0 +1,70 @@ +fetchFollowerQuery = $fetchFollowerQuery; + $this->accessControl = $accessControl; + $this->fetchInvitationQuery = $fetchInvitationQuery; + $this->userRoleLocator = $userRoleLocator; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + $feedId = $model->getFeedId(); + $token = $model->getAccessToken(); + + $invitation = $this->fetchInvitationQuery->execute($model->getFeedId(), $model->getAuthUserId()); + + if (!$this->accessControl->hasFollowAccess($feedId, $token) && $invitation === null) { + throw new NotFound('feed not found', 'not_found'); + } + + $follower = $this->fetchFollowerQuery->execute($feedId, $model->getAuthUserId()); + + if ($follower !== null) { + $model->setFollowing(true); + } + + if ($invitation !== null) { + $model->setRole($this->userRoleLocator->locate($invitation['role'])); + } + } + } +} diff --git a/API/src/Handlers/Post/Feed/FollowRequestHandler.php b/API/src/Handlers/Post/Feed/FollowRequestHandler.php new file mode 100644 index 0000000..387c51e --- /dev/null +++ b/API/src/Handlers/Post/Feed/FollowRequestHandler.php @@ -0,0 +1,25 @@ +getUri()->getExplodedPath(); + $feedId = new FeedId($parts[2]); + + $model->setFeedId($feedId); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Invitations/CommandHandler.php b/API/src/Handlers/Post/Feed/Invitations/CommandHandler.php new file mode 100644 index 0000000..f958b8f --- /dev/null +++ b/API/src/Handlers/Post/Feed/Invitations/CommandHandler.php @@ -0,0 +1,45 @@ +createInvitationCommand = $createInvitationCommand; + $this->documentMapper = $documentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var \Timetabio\API\Models\Feed\Invitation\CreateModel $model */ + + $invitation = new \Timetabio\Library\DataObjects\FeedInvitation( + $model->getInvitationFeedId(), + $model->getInvitationUserId(), + $model->getInvitationUserRole() + ); + + $result = $this->createInvitationCommand->execute($invitation); + + $model->setData($this->documentMapper->map($result)); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Invitations/QueryHandler.php b/API/src/Handlers/Post/Feed/Invitations/QueryHandler.php new file mode 100644 index 0000000..9170405 --- /dev/null +++ b/API/src/Handlers/Post/Feed/Invitations/QueryHandler.php @@ -0,0 +1,86 @@ +accessControl = $accessControl; + $this->invitationExistsQuery = $invitationExistsQuery; + $this->fetchFeedUserQuery = $fetchFeedUserQuery; + $this->fetchUserByUsernameQuery = $fetchUserByUsernameQuery; + } + + public function execute(AbstractModel $model) + { + /** @var \Timetabio\API\Models\Feed\Invitation\CreateModel $model */ + + $feedId = $model->getInvitationFeedId(); + $username = $model->getInvitationUsername(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + if (!$this->accessControl->hasWriteAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + + $user = $this->fetchUserByUsernameQuery->execute($username); + + if ($user === null) { + throw new BadRequest('user does not exist', 'user_not_found'); + } + + if ($this->invitationExistsQuery->execute($feedId, $user['id'])) { + throw new BadRequest('invitation already exists', 'invitation_exists'); + } + + if ($this->fetchFeedUserQuery->execute($feedId, $user['id']) !== null) { + throw new BadRequest('user is already added to feed', 'already_added'); + } + + $model->setInvitationUserId($user['id']); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Invitations/RequestHandler.php b/API/src/Handlers/Post/Feed/Invitations/RequestHandler.php new file mode 100644 index 0000000..780609d --- /dev/null +++ b/API/src/Handlers/Post/Feed/Invitations/RequestHandler.php @@ -0,0 +1,54 @@ +userRoleLocator = $userRoleLocator; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var \Timetabio\Framework\Http\Request\PostRequest $request */ + /** @var \Timetabio\API\Models\Feed\Invitation\CreateModel $model */ + + $parts = $request->getUri()->getExplodedPath(); + + if (!$request->hasParam('username')) { + throw new BadRequest('required parameter \'username\' is missing', 'parameter_missing'); + } + + if (!$request->hasParam('role')) { + throw new BadRequest('required parameter \'role\' is missing', 'parameter_missing'); + } + + $username = $request->getParam('username'); + + try { + $role = $this->userRoleLocator->locate($request->getParam('role')); + } catch (\Exception $exception) { + throw new BadRequest('invalid role', 'invalid_role'); + } + + $model->setInvitationFeedId($parts[2]); + $model->setInvitationUsername($username); + $model->setInvitationUserRole($role); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Unfollow/CommandHandler.php b/API/src/Handlers/Post/Feed/Unfollow/CommandHandler.php new file mode 100644 index 0000000..4d58d81 --- /dev/null +++ b/API/src/Handlers/Post/Feed/Unfollow/CommandHandler.php @@ -0,0 +1,38 @@ +unfollowFeedCommand = $unfollowFeedCommand; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + if ($model->isFollowing()) { + $this->unfollowFeedCommand->execute($model->getFeedId(), $model->getAuthUserId()); + } + + $model->setData([ + 'feed_id' => $model->getFeedId() + ]); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Unfollow/QueryHandler.php b/API/src/Handlers/Post/Feed/Unfollow/QueryHandler.php new file mode 100644 index 0000000..6c9e91c --- /dev/null +++ b/API/src/Handlers/Post/Feed/Unfollow/QueryHandler.php @@ -0,0 +1,56 @@ +fetchFollowerQuery = $fetchFollowerQuery; + $this->accessControl = $accessControl; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + $feedId = $model->getFeedId(); + $token = $model->getAccessToken(); + $userId = $model->getAuthUserId(); + + if (!$this->accessControl->hasFollowAccess($feedId, $token)) { + throw new NotFound('feed not found', 'not_found'); + } + + $follower = $this->fetchFollowerQuery->execute($feedId, $userId); + + if ($follower !== null) { + $model->setFollowing(true); + } + + if (!$this->accessControl->canUnfollow($feedId, $token)) { + throw new BadRequest('cannot unfollow (e.g. last owner in feed)', 'unfollow_error'); + } + } + } +} diff --git a/API/src/Handlers/Post/Feed/Upload/CommandHandler.php b/API/src/Handlers/Post/Feed/Upload/CommandHandler.php new file mode 100644 index 0000000..1e36b14 --- /dev/null +++ b/API/src/Handlers/Post/Feed/Upload/CommandHandler.php @@ -0,0 +1,53 @@ +createFeedUploadUrlCommand = $createFeedUploadUrlCommand; + $this->createFileCommand = $createFileCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UploadModel $model */ + + $uploadParams = $this->createFeedUploadUrlCommand->execute( + $model->getFilename(), + $model->getMimeType() + ); + + $this->createFileCommand->execute( + $model->getAuthUserId(), + $uploadParams->getFile(), + $model->getMimeType() + ); + + $model->setData([ + 'public_id' => (string) $uploadParams->getFile()->getPublicId(), + 'endpoint' => $uploadParams->getEndpoint(), + 'params' => $uploadParams->getParams() + ]); + } + } +} diff --git a/API/src/Handlers/Post/Feed/Upload/RequestHandler.php b/API/src/Handlers/Post/Feed/Upload/RequestHandler.php new file mode 100644 index 0000000..31678bd --- /dev/null +++ b/API/src/Handlers/Post/Feed/Upload/RequestHandler.php @@ -0,0 +1,33 @@ +hasParam('mime_type')) { + throw new BadRequest('missing mime type', 'missing_mime_type'); + } + + if (!$request->hasParam('filename')) { + throw new BadRequest('missing filename', 'missing_filename'); + } + + $model->setFilename($request->getParam('filename')); + $model->setMimeType($request->getParam('mime_type')); + } + } +} diff --git a/API/src/Handlers/Post/Feeds/CommandHandler.php b/API/src/Handlers/Post/Feeds/CommandHandler.php new file mode 100644 index 0000000..03b5345 --- /dev/null +++ b/API/src/Handlers/Post/Feeds/CommandHandler.php @@ -0,0 +1,46 @@ +createFeedCommand = $createFeedCommand; + $this->documentMapper = $documentMapper; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $owner = $model->getAuthUserId(); + $description = $model->getDescription(); + $name = $model->getName(); + $private = $model->isPrivate(); + + $feed = $this->createFeedCommand->execute($owner, $name, $description, $private); + + $model->setData($this->documentMapper->map($feed)); + $model->setStatusCode(new \Timetabio\Framework\Http\StatusCodes\Created); + } + } +} diff --git a/API/src/Handlers/Post/Feeds/RequestHandler.php b/API/src/Handlers/Post/Feeds/RequestHandler.php new file mode 100644 index 0000000..6be8af5 --- /dev/null +++ b/API/src/Handlers/Post/Feeds/RequestHandler.php @@ -0,0 +1,62 @@ +hasParam('name')) { + throw new BadRequest('missing parameter \'name\'', 'missing_parameter'); + } + + if (!$request->hasParam('is_private')) { + throw new BadRequest('missing parameter \'is_private\'', 'missing_parameter'); + } + + try { + $name = new FeedName($request->getParam('name')); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_feed_name', $exception); + } + + try { + $private = new StringBoolean($request->getParam('is_private')); + } catch (\Exception $exception) { + throw new BadRequest('is_private must be a boolean', 'invalid_feed_is_private'); + } + + $model->setName($name); + $model->setPrivate($private->getValue()); + $model->setDescription($this->getDescription($request)); + } + + private function getDescription(PostRequest $request): FeedDescription + { + if (!$request->hasParam('description')) { + return new FeedDescription; + } + + try { + return new FeedDescription($request->getParam('description')); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_feed_description', $exception); + } + } + } +} diff --git a/API/src/Handlers/Post/Posts/CommandHandler.php b/API/src/Handlers/Post/Posts/CommandHandler.php new file mode 100644 index 0000000..8dc1837 --- /dev/null +++ b/API/src/Handlers/Post/Posts/CommandHandler.php @@ -0,0 +1,61 @@ +createPostCommand = $createPostCommand; + $this->postMapper = $postMapper; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + if ($model->getPostType() instanceof \Timetabio\Library\PostTypes\Event && $model->getPostTimestamp() === null) { + throw new BadRequest('required parameter \'timestamp\' missing', 'parameter_missing'); + } + + if ($model->getPostTitle() === '') { + throw new BadRequest('empty post title', 'empty_post_title'); + } + + $post = $this->createPostCommand->execute( + $model->getPostType(), + $model->getPostTitle(), + $model->getPostBody(), + $model->getFeedId(), + $model->getAuthUserId(), + $model->getPostTimestamp(), + $model->getPostAttachments() + ); + + $model->setData( + $this->postMapper->map($post) + ); + + $model->setStatusCode(new \Timetabio\Framework\Http\StatusCodes\Created); + } + } +} diff --git a/API/src/Handlers/Post/Posts/QueryHandler.php b/API/src/Handlers/Post/Posts/QueryHandler.php new file mode 100644 index 0000000..6dce7ec --- /dev/null +++ b/API/src/Handlers/Post/Posts/QueryHandler.php @@ -0,0 +1,63 @@ +accessControl = $accessControl; + $this->fetchFileByPublicIdQuery = $fetchFileByPathQuery; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $feedId = $model->getFeedId(); + $accessToken = $model->getAccessToken(); + + if (!$this->accessControl->hasReadAccess($feedId, $accessToken)) { + throw new NotFound('feed not found', 'not_found'); + } + + if (!$this->accessControl->hasPostAccess($feedId, $accessToken)) { + throw new Forbidden('access denied', 'access_denied'); + } + + /** @var Attachment $attachment */ + foreach ($model->getPostAttachments() as $attachment) { + $publicId = $attachment->getPublicId(); + $file = $this->fetchFileByPublicIdQuery->execute($publicId); + + if ($file === null) { + throw new BadRequest('file \'' . $publicId . '\' not found', 'file_not_found'); + } + + $attachment->setFileId($file['id']); + } + } + } +} diff --git a/API/src/Handlers/Post/Posts/RequestHandler.php b/API/src/Handlers/Post/Posts/RequestHandler.php new file mode 100644 index 0000000..7bf406e --- /dev/null +++ b/API/src/Handlers/Post/Posts/RequestHandler.php @@ -0,0 +1,125 @@ +postTypeLocator = $postTypeLocator; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var PostRequest $request */ + /** @var CreateModel $model */ + + $this->checkRequiredArguments($request); + + $model->setPostType($this->getPostType($request)); + + $parts = $request->getUri()->getExplodedPath(); + $model->setFeedId($parts[2]); + + $model->setPostTitle($this->getPostTitle($request)); + $model->setPostBody($this->getPostBody($request)); + + if ($request->hasParam('timestamp')) { + $model->setPostTimestamp($this->getTimestamp($request)); + } + + $this->addAttachments($request, $model); + } + + private function checkRequiredArguments(PostRequest $request) + { + foreach ($this->requiredParams as $param) { + if (!$request->hasParam($param)) { + throw new BadRequest('missing parameter \'' . $param . '\'', 'missing_parameter'); + } + } + } + + private function getPostType(PostRequest $request): PostTypeInterface + { + try { + return $this->postTypeLocator->locate($request->getParam('type')); + } catch (\Exception $exception) { + throw new BadRequest('invalid post type', 'invalid_type'); + } + } + + private function getTimestamp(PostRequest $request): Timestamp + { + $timestamp = $request->getParam('timestamp'); + + if (!ctype_digit($timestamp)) { + throw new BadRequest('invalid timestamp', 'invalid_timestamp'); + } + + return new Timestamp($timestamp); + } + + private function getPostBody(PostRequest $request): PostBody + { + try { + return new PostBody($request->getParam('body')); + } catch (\Exception $exception) { + throw new BadRequest('post body limit of 8 kib exceeded', 'invalid_post_body'); + } + } + + private function getPostTitle(PostRequest $request): PostTitle + { + try { + return new PostTitle($request->getParam('title')); + } catch (\Exception $exception) { + throw new BadRequest($exception->getMessage(), 'invalid_post_title', $exception); + } + } + + private function addAttachments(PostRequest $request, CreateModel $model) + { + if (!$request->hasParam('attachments')) { + return; + } + + if (!is_array($request->getParam('attachments'))) { + throw new BadRequest('attachments must be an array', 'invalid_attachments'); + } + + foreach ($request->getParam('attachments') as $attachment) { + $model->addPostAttachment(new Attachment($attachment)); + } + } + } +} diff --git a/API/src/Handlers/Post/Revoke/CommandHandler.php b/API/src/Handlers/Post/Revoke/CommandHandler.php new file mode 100644 index 0000000..2423b92 --- /dev/null +++ b/API/src/Handlers/Post/Revoke/CommandHandler.php @@ -0,0 +1,35 @@ +deleteAccessTokenCommand = $deleteAccessTokenCommand; + } + + public function execute(AbstractModel $model) + { + /** @var APIModel $model */ + + $this->deleteAccessTokenCommand->execute($model->getAccessToken()); + + $model->setData([ + 'revoked' => true + ]); + } + } +} diff --git a/API/src/Handlers/Post/Users/CommandHandler.php b/API/src/Handlers/Post/Users/CommandHandler.php new file mode 100644 index 0000000..5802364 --- /dev/null +++ b/API/src/Handlers/Post/Users/CommandHandler.php @@ -0,0 +1,61 @@ +createUserCommand = $createUserCommand; + $this->sendVerificationCommand = $sendVerificationCommand; + $this->getUserMapper = $getUserMapper; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $token = new Token; + + $email = $model->getEmail(); + $username = $model->getUsername(); + + $user = $this->createUserCommand->execute($email, $username, $model->getPassword(), $token); + $mappedUser = $this->getUserMapper->map($user); + + $this->sendVerificationCommand->execute(new EmailPerson($email, $username), $token); + + $model->setData($mappedUser); + } + } +} diff --git a/API/src/Handlers/Post/Users/QueryHandler.php b/API/src/Handlers/Post/Users/QueryHandler.php new file mode 100644 index 0000000..09cc981 --- /dev/null +++ b/API/src/Handlers/Post/Users/QueryHandler.php @@ -0,0 +1,66 @@ +fetchUserByEmailQuery = $fetchUserByEmailQuery; + $this->fetchUserByUsernameQuery = $fetchUserByUsernameQuery; + $this->isInvitedQuery = $isInvitedQuery; + } + + public function execute(AbstractModel $model) + { + /** @var CreateModel $model */ + + $email = $model->getEmail(); + + if (!$this->isInvitedQuery->execute($email)) { + throw new BadRequest('email is not invited', 'email_not_invited'); + } + + $userByEmail = $this->fetchUserByEmailQuery->execute($email); + + if ($userByEmail !== null) { + throw new BadRequest('email already registered', 'email_already_registered'); + } + + $userByUsername = $this->fetchUserByUsernameQuery->execute($model->getUsername()); + + if ($userByUsername !== null) { + throw new BadRequest('username already registered', 'username_taken'); + } + } + } +} diff --git a/API/src/Handlers/Post/Users/RequestHandler.php b/API/src/Handlers/Post/Users/RequestHandler.php new file mode 100644 index 0000000..372c4fa --- /dev/null +++ b/API/src/Handlers/Post/Users/RequestHandler.php @@ -0,0 +1,47 @@ +getParam('email')); + } catch (\Exception $e) { + throw new BadRequest('invalid email address', 'invalid_email'); + } + + try { + $password = new Password($request->getParam('password')); + } catch (\Exception $e) { + throw new BadRequest('password must be between 8 and 72 characters', 'invalid_password'); + } + + try { + $username = new Username($request->getParam('username')); + } catch (\Exception $e) { + throw new BadRequest('invalid username', 'invalid_username'); + } + + $model->setEmail($email); + $model->setPassword($password); + $model->setUsername($username); + } + } +} diff --git a/API/src/Handlers/Post/Verify/CommandHandler.php b/API/src/Handlers/Post/Verify/CommandHandler.php new file mode 100644 index 0000000..774e018 --- /dev/null +++ b/API/src/Handlers/Post/Verify/CommandHandler.php @@ -0,0 +1,38 @@ +verifyUserCommand = $verifyUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var APIModel $model */ + /** @var PostRequest $request */ + + $this->verifyUserCommand->execute($model->getAuthUserId()); + + $model->setData([ + 'id' => (string) $model->getAuthUserId(), + 'is_verified' => true + ]); + } + } +} diff --git a/API/src/Handlers/Post/Verify/QueryHandler.php b/API/src/Handlers/Post/Verify/QueryHandler.php new file mode 100644 index 0000000..26e98e6 --- /dev/null +++ b/API/src/Handlers/Post/Verify/QueryHandler.php @@ -0,0 +1,41 @@ +fetchUserByTokenQuery = $fetchUserByTokenQuery; + } + + public function execute(AbstractModel $model) + { + /** @var VerifyModel $model */ + /** @var PostRequest $request */ + + $token = $this->fetchUserByTokenQuery->execute($model->getToken()); + + if ($token === null) { + throw new BadRequest('token does not exist', 'invalid_token'); + } + + $model->setAuthUserId(new UserId($token['user_id'])); + } + } +} diff --git a/API/src/Handlers/Post/Verify/RequestHandler.php b/API/src/Handlers/Post/Verify/RequestHandler.php new file mode 100644 index 0000000..af0fdb9 --- /dev/null +++ b/API/src/Handlers/Post/Verify/RequestHandler.php @@ -0,0 +1,29 @@ +hasParam('token')) { + throw new BadRequest('missing parameter \'token\'', 'missing_parameter'); + } + + $model->setToken(new Token($request->getParam('token'))); + } + } +} diff --git a/API/src/Handlers/Post/Verify/Resend/CommandHandler.php b/API/src/Handlers/Post/Verify/Resend/CommandHandler.php new file mode 100644 index 0000000..540b40d --- /dev/null +++ b/API/src/Handlers/Post/Verify/Resend/CommandHandler.php @@ -0,0 +1,34 @@ +sendVerificationCommand = $sendVerificationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var ResendModel $model */ + + $this->sendVerificationCommand->execute( + $model->getEmailPerson(), + $model->getToken() + ); + } + } +} diff --git a/API/src/Handlers/Post/Verify/Resend/QueryHandler.php b/API/src/Handlers/Post/Verify/Resend/QueryHandler.php new file mode 100644 index 0000000..e6c6b0a --- /dev/null +++ b/API/src/Handlers/Post/Verify/Resend/QueryHandler.php @@ -0,0 +1,48 @@ +fetchTokenQuery = $fetchTokenQuery; + } + + public function execute(AbstractModel $model) + { + /** @var ResendModel $model */ + + $email = $model->getEmail(); + + $token = $this->fetchTokenQuery->execute($email); + + if ($token === null) { + throw new BadRequest('no token found for email', 'email_not_found'); + } + + $model->setEmailPerson(new EmailPerson($email)); + $model->setToken(new Token($token['token'])); + + $model->setData([ + 'id' => $token['user_id'], + 'verified' => false + ]); + } + } +} diff --git a/API/src/Handlers/Post/Verify/Resend/RequestHandler.php b/API/src/Handlers/Post/Verify/Resend/RequestHandler.php new file mode 100644 index 0000000..553ec90 --- /dev/null +++ b/API/src/Handlers/Post/Verify/Resend/RequestHandler.php @@ -0,0 +1,35 @@ +hasParam('email')) { + throw new BadRequest('required parameter \'email\' missing', 'parameter_missing'); + } + + try { + $email = new EmailAddress($request->getParam('email')); + } catch (\Exception $exception) { + throw new BadRequest('invalid email', 'invalid_email'); + } + + $model->setEmail($email); + } + } +} diff --git a/API/src/Handlers/PostHandler.php b/API/src/Handlers/PostHandler.php new file mode 100644 index 0000000..ff006b8 --- /dev/null +++ b/API/src/Handlers/PostHandler.php @@ -0,0 +1,41 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function execute(AbstractModel $model) + { + /** @var APIModel $model */ + + if (!$model->hasAccessToken()) { + return; + } + + $accessToken = $model->getAccessToken(); + + if (!$accessToken->getAutoRenew()) { + return; + } + + $this->dataStoreWriter->renewAccessToken($accessToken); + } + } +} diff --git a/API/src/Handlers/PreHandler.php b/API/src/Handlers/PreHandler.php new file mode 100644 index 0000000..965a263 --- /dev/null +++ b/API/src/Handlers/PreHandler.php @@ -0,0 +1,55 @@ +dataStoreReader = $dataStoreReader; + $this->requestTokenReader = $requestTokenReader; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var APIModel $model */ + + $token = $this->requestTokenReader->read($request); + + if ($token === null) { + return; + } + + if (!$this->dataStoreReader->hasAccessToken($token)) { + return; + } + + $accessToken = $this->dataStoreReader->getAccessToken($token); + + $model->setAccessToken($accessToken); + + if ($accessToken->hasUserId()) { + $model->setAuthUserId($accessToken->getUserId()); + } + } + } +} diff --git a/API/src/Handlers/Put/User/CommandHandler.php b/API/src/Handlers/Put/User/CommandHandler.php new file mode 100644 index 0000000..9574298 --- /dev/null +++ b/API/src/Handlers/Put/User/CommandHandler.php @@ -0,0 +1,33 @@ +updateUserCommand = $updateUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateUserPasswordModel $model */ + + $this->updateUserCommand->execute($model->getAuthUserId(), ['password' => (string) new Hash($model->getNewPassword())]); + + $model->setData(['updated' => true]); + } + } +} diff --git a/API/src/Handlers/Put/User/QueryHandler.php b/API/src/Handlers/Put/User/QueryHandler.php new file mode 100644 index 0000000..4405ec5 --- /dev/null +++ b/API/src/Handlers/Put/User/QueryHandler.php @@ -0,0 +1,33 @@ +fetchUserPasswordQuery = $fetchUserPasswordQuery; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateUserPasswordModel $model */ + + $password = $this->fetchUserPasswordQuery->execute($model->getAuthUserId()); + + if (!password_verify($model->getOldPassword(), $password)) { + throw new BadRequest('invalid old password', 'invalid_old_password'); + } + } + } +} diff --git a/API/src/Handlers/Put/User/RequestHandler.php b/API/src/Handlers/Put/User/RequestHandler.php new file mode 100644 index 0000000..e058ab6 --- /dev/null +++ b/API/src/Handlers/Put/User/RequestHandler.php @@ -0,0 +1,37 @@ +hasParam('old_password')) { + throw new BadRequest('missing parameter \'old_password\'', 'missing_parameter'); + } + + if (!$request->hasParam('password')) { + throw new BadRequest('missing parameter \'password\'', 'missing_parameter'); + } + + try { + $password = new Password($request->getParam('password')); + } catch(\Exception $exception) { + throw new BadRequest('password must be between 8 and 72 characters', 'invalid_password'); + } + + $model->setOldPassword($request->getParam('old_password')); + $model->setNewPassword($password); + } + } +} diff --git a/API/src/Handlers/QueryHandler.php b/API/src/Handlers/QueryHandler.php new file mode 100644 index 0000000..c54d4c5 --- /dev/null +++ b/API/src/Handlers/QueryHandler.php @@ -0,0 +1,17 @@ +hasStatusCode()) { + $response->setStatusCode($model->getStatusCode()); + } + } + } +} diff --git a/API/src/Handlers/TransformationHandler.php b/API/src/Handlers/TransformationHandler.php new file mode 100644 index 0000000..bfda584 --- /dev/null +++ b/API/src/Handlers/TransformationHandler.php @@ -0,0 +1,20 @@ +getData(), JSON_PRETTY_PRINT) . PHP_EOL; + } + } +} diff --git a/API/src/Locators/PostTypeLocator.php b/API/src/Locators/PostTypeLocator.php new file mode 100644 index 0000000..cc48a35 --- /dev/null +++ b/API/src/Locators/PostTypeLocator.php @@ -0,0 +1,25 @@ +documentMapper = $documentMapper; + } + + public function map(array $user): array + { + $mapped = $this->documentMapper->map($user); + + if (isset($mapped['user_id'])) { + $mapped['user']['id'] = $mapped['user_id']; + unset($mapped['user_id']); + } + + if (isset($mapped['name'])) { + $mapped['user']['name'] = $mapped['name']; + unset($mapped['name']); + } + + if (isset($mapped['username'])) { + $mapped['user']['username'] = $mapped['username']; + unset($mapped['username']); + } + + unset($mapped['feed_id']); + + return $mapped; + } + } +} diff --git a/API/src/Mappers/PostAttachmentMapper.php b/API/src/Mappers/PostAttachmentMapper.php new file mode 100644 index 0000000..79638e2 --- /dev/null +++ b/API/src/Mappers/PostAttachmentMapper.php @@ -0,0 +1,40 @@ +uriBuilder = $uriBuilder; + } + + public function map(array $attachment): array + { + $mapped = []; + + if (isset($attachment['filename'])) { + $mapped['filename'] = $attachment['filename']; + } + + if (isset($attachment['mime_type'])) { + $mapped['mime_type'] = $attachment['mime_type']; + } + + if (isset($attachment['public_id']) && isset($attachment['filename'])) { + $mapped['url'] = $this->uriBuilder->buildFileUri($attachment['public_id'], $attachment['filename']); + } + + return $mapped; + } + } +} diff --git a/API/src/Mappers/ResultsMapper.php b/API/src/Mappers/ResultsMapper.php new file mode 100644 index 0000000..0ee55a7 --- /dev/null +++ b/API/src/Mappers/ResultsMapper.php @@ -0,0 +1,27 @@ +mapResult($result); + } + + return $mapped; + } + + protected function mapResult(array $result): array + { + unset($result['_source']['_feed_id']); + + return $result['_source']; + } + } +} diff --git a/API/src/Mappers/SearchResultsMapper.php b/API/src/Mappers/SearchResultsMapper.php new file mode 100644 index 0000000..0c2c70d --- /dev/null +++ b/API/src/Mappers/SearchResultsMapper.php @@ -0,0 +1,21 @@ + $result['_type'], + 'data' => $result['_source'] + ]; + + return $mapped; + } + } +} diff --git a/API/src/Models/APIModel.php b/API/src/Models/APIModel.php new file mode 100644 index 0000000..8a7f2d4 --- /dev/null +++ b/API/src/Models/APIModel.php @@ -0,0 +1,95 @@ +data; + } + + /** + * @param \JsonSerializable|array $data + */ + public function setData($data) + { + $this->data = $data; + } + + public function hasStatusCode(): bool + { + return $this->statusCode !== null; + } + + public function getStatusCode(): StatusCodeInterface + { + return $this->statusCode; + } + + public function setStatusCode(StatusCodeInterface $statusCode) + { + $this->statusCode = $statusCode; + } + + public function hasAccessToken(): bool + { + return $this->accessToken !== null; + } + + public function getAccessToken(): AccessToken + { + return $this->accessToken; + } + + public function setAccessToken(AccessToken $accessToken) + { + $this->accessToken = $accessToken; + } + + public function hasAuthUserId(): bool + { + return $this->authUserId !== null; + } + + public function getAuthUserId(): UserId + { + return $this->authUserId; + } + + public function setAuthUserId(UserId $authUserId) + { + $this->authUserId = $authUserId; + } + } +} diff --git a/API/src/Models/AuthModel.php b/API/src/Models/AuthModel.php new file mode 100644 index 0000000..6a0fb9e --- /dev/null +++ b/API/src/Models/AuthModel.php @@ -0,0 +1,71 @@ +user; + } + + public function setUser(string $user) + { + $this->user = $user; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password) + { + $this->password = $password; + } + + public function getAccessType(): AccessTypeInterface + { + return $this->accessType; + } + + public function setAccessType(AccessTypeInterface $accessType) + { + $this->accessType = $accessType; + } + + public function setAutoRenew(bool $autoRenew) + { + $this->autoRenew = $autoRenew; + } + + public function getAutoRenew(): bool + { + return $this->autoRenew; + } + } +} diff --git a/API/src/Models/BetaRequest/CreateModel.php b/API/src/Models/BetaRequest/CreateModel.php new file mode 100644 index 0000000..f41d57f --- /dev/null +++ b/API/src/Models/BetaRequest/CreateModel.php @@ -0,0 +1,27 @@ +email; + } + + public function setEmail(EmailAddress $email) + { + $this->email = $email; + } + } +} diff --git a/API/src/Models/Collection/CollectionModel.php b/API/src/Models/Collection/CollectionModel.php new file mode 100644 index 0000000..b81c8bd --- /dev/null +++ b/API/src/Models/Collection/CollectionModel.php @@ -0,0 +1,27 @@ +collectionId; + } + + public function setCollectionId(CollectionId $collectionId) + { + $this->collectionId = $collectionId; + } + } +} diff --git a/API/src/Models/Collection/CreateModel.php b/API/src/Models/Collection/CreateModel.php new file mode 100644 index 0000000..85331d5 --- /dev/null +++ b/API/src/Models/Collection/CreateModel.php @@ -0,0 +1,27 @@ +collectionName; + } + + public function setCollectionName(CollectionName $collectionName) + { + $this->collectionName = $collectionName; + } + } +} diff --git a/API/src/Models/Collection/UpdateModel.php b/API/src/Models/Collection/UpdateModel.php new file mode 100644 index 0000000..969d6ff --- /dev/null +++ b/API/src/Models/Collection/UpdateModel.php @@ -0,0 +1,13 @@ +name; + } + + public function setName(FeedName $name) + { + $this->name = $name; + } + + public function isPrivate(): bool + { + return $this->private; + } + + public function setPrivate(bool $private) + { + $this->private = $private; + } + + public function getDescription(): FeedDescription + { + return $this->description; + } + + public function setDescription(FeedDescription $description) + { + $this->description = $description; + } + } +} diff --git a/API/src/Models/Feed/FeedModel.php b/API/src/Models/Feed/FeedModel.php new file mode 100644 index 0000000..53a1268 --- /dev/null +++ b/API/src/Models/Feed/FeedModel.php @@ -0,0 +1,27 @@ +feedId; + } + + public function setFeedId(FeedId $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/API/src/Models/Feed/FollowModel.php b/API/src/Models/Feed/FollowModel.php new file mode 100644 index 0000000..4b1f004 --- /dev/null +++ b/API/src/Models/Feed/FollowModel.php @@ -0,0 +1,46 @@ +role = new \Timetabio\Library\UserRoles\DefaultUserRole; + } + + public function isFollowing(): bool + { + return $this->following; + } + + public function setFollowing(bool $following) + { + $this->following = $following; + } + + public function getRole(): UserRole + { + return $this->role; + } + + public function setRole(UserRole $role) + { + $this->role = $role; + } + } +} diff --git a/API/src/Models/Feed/Invitation/CreateModel.php b/API/src/Models/Feed/Invitation/CreateModel.php new file mode 100644 index 0000000..6bfa380 --- /dev/null +++ b/API/src/Models/Feed/Invitation/CreateModel.php @@ -0,0 +1,73 @@ +invitationFeedId; + } + + public function setInvitationFeedId(string $invitationFeedId) + { + $this->invitationFeedId = $invitationFeedId; + } + + public function getInvitationUsername(): string + { + return $this->invitationUsername; + } + + public function setInvitationUsername(string $invitationUsername) + { + $this->invitationUsername = $invitationUsername; + } + + public function getInvitationUserId(): string + { + return $this->invitationUserId; + } + + public function setInvitationUserId(string $invitationUserId) + { + $this->invitationUserId = $invitationUserId; + } + + public function getInvitationUserRole(): UserRole + { + return $this->invitationUserRole; + } + + public function setInvitationUserRole(UserRole $invitationUserRole) + { + $this->invitationUserRole = $invitationUserRole; + } + } +} diff --git a/API/src/Models/Feed/Invitation/DeleteModel.php b/API/src/Models/Feed/Invitation/DeleteModel.php new file mode 100644 index 0000000..776f58c --- /dev/null +++ b/API/src/Models/Feed/Invitation/DeleteModel.php @@ -0,0 +1,26 @@ +userId; + } + + public function setUserId(string $userId) + { + $this->userId = $userId; + } + } +} diff --git a/API/src/Models/Feed/Invitation/UpdateModel.php b/API/src/Models/Feed/Invitation/UpdateModel.php new file mode 100644 index 0000000..4a9f760 --- /dev/null +++ b/API/src/Models/Feed/Invitation/UpdateModel.php @@ -0,0 +1,14 @@ +userId; + } + + public function setUserId(UserId $userId) + { + $this->userId = $userId; + } + } +} diff --git a/API/src/Models/Feed/People/ListModel.php b/API/src/Models/Feed/People/ListModel.php new file mode 100644 index 0000000..cc8b10f --- /dev/null +++ b/API/src/Models/Feed/People/ListModel.php @@ -0,0 +1,24 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/API/src/Models/Feed/Posts/ListModel.php b/API/src/Models/Feed/Posts/ListModel.php new file mode 100644 index 0000000..529bd8c --- /dev/null +++ b/API/src/Models/Feed/Posts/ListModel.php @@ -0,0 +1,26 @@ +feedId; + } + + public function setFeedId(FeedId $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/API/src/Models/Feed/UpdateModel.php b/API/src/Models/Feed/UpdateModel.php new file mode 100644 index 0000000..8ffd55e --- /dev/null +++ b/API/src/Models/Feed/UpdateModel.php @@ -0,0 +1,33 @@ +feedVanity !== null; + } + + public function getFeedVanity(): string + { + return $this->feedVanity; + } + + public function setFeedVanity(string $feedVanity) + { + $this->feedVanity = $feedVanity; + } + } +} diff --git a/API/src/Models/Feed/UploadModel.php b/API/src/Models/Feed/UploadModel.php new file mode 100644 index 0000000..ac0fd79 --- /dev/null +++ b/API/src/Models/Feed/UploadModel.php @@ -0,0 +1,56 @@ +mimeType; + } + + public function setMimeType(string $mimeType) + { + $this->mimeType = $mimeType; + } + + public function getFilename(): string + { + return $this->filename; + } + + public function setFilename(string $filename) + { + $this->filename = $filename; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/API/src/Models/Feed/User/UpdateModel.php b/API/src/Models/Feed/User/UpdateModel.php new file mode 100644 index 0000000..bee0b42 --- /dev/null +++ b/API/src/Models/Feed/User/UpdateModel.php @@ -0,0 +1,42 @@ +userId; + } + + public function setUserId(string $userId) + { + $this->userId = $userId; + } + + public function getRole(): UserRole + { + return $this->role; + } + + public function setRole(UserRole $role) + { + $this->role = $role; + } + } +} diff --git a/API/src/Models/ListModel.php b/API/src/Models/ListModel.php new file mode 100644 index 0000000..018750c --- /dev/null +++ b/API/src/Models/ListModel.php @@ -0,0 +1,39 @@ +limit; + } + + public function setLimit(int $limit) + { + $this->limit = $limit; + } + + public function getPage(): int + { + return $this->page; + } + + public function setPage(int $page) + { + $this->page = $page; + } + } +} diff --git a/API/src/Models/Post/CreateModel.php b/API/src/Models/Post/CreateModel.php new file mode 100644 index 0000000..f444571 --- /dev/null +++ b/API/src/Models/Post/CreateModel.php @@ -0,0 +1,104 @@ +postType; + } + + public function setPostType(PostTypeInterface $postType) + { + $this->postType = $postType; + } + + public function getPostTitle(): string + { + return $this->postTitle; + } + + public function setPostTitle(string $postTitle) + { + $this->postTitle = $postTitle; + } + + public function getPostBody(): string + { + return $this->postBody; + } + + public function setPostBody(string $postBody) + { + $this->postBody = $postBody; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getPostTimestamp() + { + return $this->postTimestamp; + } + + public function setPostTimestamp(Timestamp $postTimestamp) + { + $this->postTimestamp = $postTimestamp; + } + + public function addPostAttachment(Attachment $attachment) + { + $this->postAttachments[] = $attachment; + } + + public function getPostAttachments(): array + { + return $this->postAttachments; + } + } +} diff --git a/API/src/Models/Post/PostModel.php b/API/src/Models/Post/PostModel.php new file mode 100644 index 0000000..753600a --- /dev/null +++ b/API/src/Models/Post/PostModel.php @@ -0,0 +1,26 @@ +postId; + } + + public function setPostId(string $postId) + { + $this->postId = $postId; + } + } +} diff --git a/API/src/Models/Profile/ProfileModel.php b/API/src/Models/Profile/ProfileModel.php new file mode 100644 index 0000000..eb9939a --- /dev/null +++ b/API/src/Models/Profile/ProfileModel.php @@ -0,0 +1,27 @@ +username; + } + + public function setUsername(Username $username) + { + $this->username = $username; + } + } +} diff --git a/API/src/Models/SearchModel.php b/API/src/Models/SearchModel.php new file mode 100644 index 0000000..7be8e96 --- /dev/null +++ b/API/src/Models/SearchModel.php @@ -0,0 +1,46 @@ +type = new \Timetabio\Library\SearchTypes\All; + } + + public function getQuery(): string + { + return $this->query; + } + + public function setQuery(string $query) + { + $this->query = $query; + } + + public function getType(): SearchType + { + return $this->type; + } + + public function setType(SearchType $type) + { + $this->type = $type; + } + } +} diff --git a/API/src/Models/UpdateModelTrait.php b/API/src/Models/UpdateModelTrait.php new file mode 100644 index 0000000..2c7bdf5 --- /dev/null +++ b/API/src/Models/UpdateModelTrait.php @@ -0,0 +1,39 @@ +updates); + } + + public function getUpdates(): array + { + return $this->updates; + } + + public function hasUpdate(string $field): bool + { + return isset($this->updates[$field]); + } + + public function getUpdate(string $field) + { + return $this->updates[$field]; + } + + public function addUpdate(string $field, $value) + { + $this->updates[$field] = $value; + } + } +} diff --git a/API/src/Models/User/CreateModel.php b/API/src/Models/User/CreateModel.php new file mode 100644 index 0000000..f2f0591 --- /dev/null +++ b/API/src/Models/User/CreateModel.php @@ -0,0 +1,59 @@ +email; + } + + public function setEmail(EmailAddress $email) + { + $this->email = $email; + } + + public function getUsername(): Username + { + return $this->username; + } + + public function setUsername(Username $username) + { + $this->username = $username; + } + + public function getPassword(): Password + { + return $this->password; + } + + public function setPassword(Password $password) + { + $this->password = $password; + } + } +} diff --git a/API/src/Models/User/UpdateModel.php b/API/src/Models/User/UpdateModel.php new file mode 100644 index 0000000..1eb0186 --- /dev/null +++ b/API/src/Models/User/UpdateModel.php @@ -0,0 +1,14 @@ +oldPassword; + } + + public function setOldPassword(string $oldPassword) + { + $this->oldPassword = $oldPassword; + } + + public function getNewPassword(): Password + { + return $this->newPassword; + } + + public function setNewPassword(Password $newPassword) + { + $this->newPassword = $newPassword; + } + } +} diff --git a/API/src/Models/Verify/ResendModel.php b/API/src/Models/Verify/ResendModel.php new file mode 100644 index 0000000..9e5792a --- /dev/null +++ b/API/src/Models/Verify/ResendModel.php @@ -0,0 +1,42 @@ +email; + } + + public function setEmail(EmailAddress $email) + { + $this->email = $email; + } + + public function setEmailPerson(EmailPerson $person) + { + $this->person = $person; + } + + public function getEmailPerson(): EmailPerson + { + return $this->person; + } + } +} diff --git a/API/src/Models/Verify/VerifyModel.php b/API/src/Models/Verify/VerifyModel.php new file mode 100644 index 0000000..cd44bc9 --- /dev/null +++ b/API/src/Models/Verify/VerifyModel.php @@ -0,0 +1,27 @@ +token; + } + + public function setToken(Token $token) + { + $this->token = $token; + } + } +} diff --git a/API/src/Queries/BetaRequest/FetchBetaRequestByEmailQuery.php b/API/src/Queries/BetaRequest/FetchBetaRequestByEmailQuery.php new file mode 100644 index 0000000..5c5be2d --- /dev/null +++ b/API/src/Queries/BetaRequest/FetchBetaRequestByEmailQuery.php @@ -0,0 +1,26 @@ +betaRequestService = $betaRequestService; + } + + public function execute(string $email) + { + return $this->betaRequestService->getBetaRequestByEmail($email); + } + } +} diff --git a/API/src/Queries/Feed/FeedExistsQuery.php b/API/src/Queries/Feed/FeedExistsQuery.php new file mode 100644 index 0000000..e3d31fa --- /dev/null +++ b/API/src/Queries/Feed/FeedExistsQuery.php @@ -0,0 +1,26 @@ +dataStoreReader = $dataStoreReader; + } + + public function execute(string $feedId) + { + return $this->dataStoreReader->hasFeed($feedId); + } + } +} diff --git a/API/src/Queries/Feed/FetchFeedUserQuery.php b/API/src/Queries/Feed/FetchFeedUserQuery.php new file mode 100644 index 0000000..67f91e0 --- /dev/null +++ b/API/src/Queries/Feed/FetchFeedUserQuery.php @@ -0,0 +1,26 @@ +peopleService = $peopleService; + } + + public function execute(string $feedId, string $userId) + { + return $this->peopleService->getPerson($feedId, $userId); + } + } +} diff --git a/API/src/Queries/Feed/FetchFeedVanityQuery.php b/API/src/Queries/Feed/FetchFeedVanityQuery.php new file mode 100644 index 0000000..7ab14f8 --- /dev/null +++ b/API/src/Queries/Feed/FetchFeedVanityQuery.php @@ -0,0 +1,26 @@ +dataStoreReader = $dataStoreReader; + } + + public function execute(string $feedId): ?string + { + return $this->dataStoreReader->getFeedVanity($feedId); + } + } +} diff --git a/API/src/Queries/Feed/FetchInvitationQuery.php b/API/src/Queries/Feed/FetchInvitationQuery.php new file mode 100644 index 0000000..0b93b16 --- /dev/null +++ b/API/src/Queries/Feed/FetchInvitationQuery.php @@ -0,0 +1,26 @@ +feedInvitationService = $feedInvitationService; + } + + public function execute(string $feedId, string $userId) + { + return $this->feedInvitationService->getInvitation($feedId, $userId); + } + } +} diff --git a/API/src/Queries/Feed/FetchInvitationsQuery.php b/API/src/Queries/Feed/FetchInvitationsQuery.php new file mode 100644 index 0000000..a4abb2a --- /dev/null +++ b/API/src/Queries/Feed/FetchInvitationsQuery.php @@ -0,0 +1,26 @@ +feedInvitationService = $feedInvitationService; + } + + public function execute(string $feedId) + { + return $this->feedInvitationService->getInvitations($feedId); + } + } +} diff --git a/API/src/Queries/Feed/FetchVanityByNameQuery.php b/API/src/Queries/Feed/FetchVanityByNameQuery.php new file mode 100644 index 0000000..fd10034 --- /dev/null +++ b/API/src/Queries/Feed/FetchVanityByNameQuery.php @@ -0,0 +1,26 @@ +feedService = $feedService; + } + + public function execute(string $name) + { + return $this->feedService->getVanityByName($name); + } + } +} diff --git a/API/src/Queries/Feed/InvitationExistsQuery.php b/API/src/Queries/Feed/InvitationExistsQuery.php new file mode 100644 index 0000000..0b7e219 --- /dev/null +++ b/API/src/Queries/Feed/InvitationExistsQuery.php @@ -0,0 +1,26 @@ +feedInvitationService = $feedInvitationService; + } + + public function execute(string $feedId, string $userId): bool + { + return $this->feedInvitationService->hasInvitation($feedId, $userId); + } + } +} diff --git a/API/src/Queries/Feeds/FetchFeedQuery.php b/API/src/Queries/Feeds/FetchFeedQuery.php new file mode 100644 index 0000000..f72cd46 --- /dev/null +++ b/API/src/Queries/Feeds/FetchFeedQuery.php @@ -0,0 +1,32 @@ +feedService = $feedService; + } + + public function execute(FeedId $feedId, UserId $userId = null) + { + if ($userId === null) { + return $this->feedService->getFeedById($feedId); + } + + return $this->feedService->getFeedByIdForUser($feedId, $userId); + } + } +} diff --git a/API/src/Queries/Feeds/FetchFeedsQuery.php b/API/src/Queries/Feeds/FetchFeedsQuery.php new file mode 100644 index 0000000..166641e --- /dev/null +++ b/API/src/Queries/Feeds/FetchFeedsQuery.php @@ -0,0 +1,26 @@ +feedService = $feedService; + } + + public function execute(int $limit, int $page = 1): array + { + return $this->feedService->getPublicFeeds($limit, $page); + } + } +} diff --git a/API/src/Queries/Feeds/FetchFollowerQuery.php b/API/src/Queries/Feeds/FetchFollowerQuery.php new file mode 100644 index 0000000..c64002e --- /dev/null +++ b/API/src/Queries/Feeds/FetchFollowerQuery.php @@ -0,0 +1,28 @@ +followerService = $followerService; + } + + public function execute(FeedId $feedId, UserId $userId) + { + return $this->followerService->getFollower($feedId, $userId); + } + } +} diff --git a/API/src/Queries/Feeds/FetchPeopleQuery.php b/API/src/Queries/Feeds/FetchPeopleQuery.php new file mode 100644 index 0000000..5fe4bf0 --- /dev/null +++ b/API/src/Queries/Feeds/FetchPeopleQuery.php @@ -0,0 +1,26 @@ +peopleService = $peopleService; + } + + public function execute(string $feedId) + { + return $this->peopleService->getPeople($feedId); + } + } +} diff --git a/API/src/Queries/Feeds/FetchPersonQuery.php b/API/src/Queries/Feeds/FetchPersonQuery.php new file mode 100644 index 0000000..c193a17 --- /dev/null +++ b/API/src/Queries/Feeds/FetchPersonQuery.php @@ -0,0 +1,28 @@ +peopleService = $peopleService; + } + + public function execute(FeedId $feedId, UserId $userId) + { + return $this->peopleService->getPerson($feedId, $userId); + } + } +} diff --git a/API/src/Queries/FetchCollectionQuery.php b/API/src/Queries/FetchCollectionQuery.php new file mode 100644 index 0000000..fbc1f62 --- /dev/null +++ b/API/src/Queries/FetchCollectionQuery.php @@ -0,0 +1,28 @@ +collectionService = $collectionService; + } + + public function execute(CollectionId $id) + { + return $this->collectionService->getCollectionById($id); + } + } +} diff --git a/API/src/Queries/File/FetchFileByPublicIdQuery.php b/API/src/Queries/File/FetchFileByPublicIdQuery.php new file mode 100644 index 0000000..0336632 --- /dev/null +++ b/API/src/Queries/File/FetchFileByPublicIdQuery.php @@ -0,0 +1,26 @@ +fileService = $fileService; + } + + public function execute(string $path) + { + return $this->fileService->getByPublicId($path); + } + } +} diff --git a/API/src/Queries/Post/FetchPostAttachmentsQuery.php b/API/src/Queries/Post/FetchPostAttachmentsQuery.php new file mode 100644 index 0000000..6388b64 --- /dev/null +++ b/API/src/Queries/Post/FetchPostAttachmentsQuery.php @@ -0,0 +1,26 @@ +postService = $postService; + } + + public function execute(string $postId) + { + return $this->postService->getPostAttachments($postId); + } + } +} diff --git a/API/src/Queries/Post/FetchPostInfoQuery.php b/API/src/Queries/Post/FetchPostInfoQuery.php new file mode 100644 index 0000000..e7871a5 --- /dev/null +++ b/API/src/Queries/Post/FetchPostInfoQuery.php @@ -0,0 +1,26 @@ +postService = $postService; + } + + public function execute(string $postId) + { + return $this->postService->getPostInfo($postId); + } + } +} diff --git a/API/src/Queries/Posts/FetchFeedPostsQuery.php b/API/src/Queries/Posts/FetchFeedPostsQuery.php new file mode 100644 index 0000000..5da4278 --- /dev/null +++ b/API/src/Queries/Posts/FetchFeedPostsQuery.php @@ -0,0 +1,26 @@ +searchBackend = $searchBackend; + } + + public function execute(string $feedId, int $limit, int $page, string $userId = null) + { + return $this->searchBackend->getFeedPosts($feedId, $limit, $page); + } + } +} diff --git a/API/src/Queries/Posts/FetchPostQuery.php b/API/src/Queries/Posts/FetchPostQuery.php new file mode 100644 index 0000000..25f6467 --- /dev/null +++ b/API/src/Queries/Posts/FetchPostQuery.php @@ -0,0 +1,48 @@ +postService = $postService; + $this->dataStoreReader = $dataStoreReader; + } + + public function execute(string $postId, string $userId = null) + { + $post = $this->fetch($postId, $userId); + + if ($post !== null) { + $post['rendered_body'] = $this->dataStoreReader->getPostBody($postId); + } + + return $post; + } + + private function fetch(string $postId, string $userId = null) + { + if ($userId === null) { + return $this->postService->getPost($postId); + } + + return $this->postService->getPostForUser($postId, $userId); + } + } +} diff --git a/API/src/Queries/Profile/FetchProfileQuery.php b/API/src/Queries/Profile/FetchProfileQuery.php new file mode 100644 index 0000000..9d8e1e4 --- /dev/null +++ b/API/src/Queries/Profile/FetchProfileQuery.php @@ -0,0 +1,26 @@ +userService = $userService; + } + + public function execute(string $username) + { + return $this->userService->getProfile($username); + } + } +} diff --git a/API/src/Queries/SearchQuery.php b/API/src/Queries/SearchQuery.php new file mode 100644 index 0000000..df4e4a4 --- /dev/null +++ b/API/src/Queries/SearchQuery.php @@ -0,0 +1,27 @@ +searchBackend = $searchBackend; + } + + public function execute(string $query, SearchType $type, string $userId, int $limit, int $page): array + { + return $this->searchBackend->search($query, $type, $userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchAuthUserQuery.php b/API/src/Queries/User/FetchAuthUserQuery.php new file mode 100644 index 0000000..cbd0453 --- /dev/null +++ b/API/src/Queries/User/FetchAuthUserQuery.php @@ -0,0 +1,26 @@ +userService = $userService; + } + + public function execute(string $user) + { + return $this->userService->getLogin($user); + } + } +} diff --git a/API/src/Queries/User/FetchTodoTasksQuery.php b/API/src/Queries/User/FetchTodoTasksQuery.php new file mode 100644 index 0000000..373630f --- /dev/null +++ b/API/src/Queries/User/FetchTodoTasksQuery.php @@ -0,0 +1,27 @@ +postService = $postService; + } + + public function execute(UserId $userId, int $limit, int $page): array + { + return $this->postService->getTodoTasks($userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchUpcomingEventsQuery.php b/API/src/Queries/User/FetchUpcomingEventsQuery.php new file mode 100644 index 0000000..73179a4 --- /dev/null +++ b/API/src/Queries/User/FetchUpcomingEventsQuery.php @@ -0,0 +1,27 @@ +postService = $postService; + } + + public function execute(UserId $userId, int $limit, int $page): array + { + return $this->postService->getUpcomingEvents($userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchUserByEmailQuery.php b/API/src/Queries/User/FetchUserByEmailQuery.php new file mode 100644 index 0000000..0dcc781 --- /dev/null +++ b/API/src/Queries/User/FetchUserByEmailQuery.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(EmailAddress $email) + { + return $this->userService->getUserByEmail($email); + } + } +} diff --git a/API/src/Queries/User/FetchUserByIdQuery.php b/API/src/Queries/User/FetchUserByIdQuery.php new file mode 100644 index 0000000..fb62ff2 --- /dev/null +++ b/API/src/Queries/User/FetchUserByIdQuery.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(string $userId) + { + return $this->userService->getUserById($userId); + } + } +} diff --git a/API/src/Queries/User/FetchUserByUsernameQuery.php b/API/src/Queries/User/FetchUserByUsernameQuery.php new file mode 100644 index 0000000..4f343a2 --- /dev/null +++ b/API/src/Queries/User/FetchUserByUsernameQuery.php @@ -0,0 +1,26 @@ +userService = $userService; + } + + public function execute(string $username) + { + return $this->userService->getUserByUsername($username); + } + } +} diff --git a/API/src/Queries/User/FetchUserCollectionsQuery.php b/API/src/Queries/User/FetchUserCollectionsQuery.php new file mode 100644 index 0000000..586efa6 --- /dev/null +++ b/API/src/Queries/User/FetchUserCollectionsQuery.php @@ -0,0 +1,27 @@ +collectionService = $collectionService; + } + + public function execute(UserId $userId, int $limit, int $page = 1): array + { + return $this->collectionService->getCollectionsByUser($userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchUserFeedQuery.php b/API/src/Queries/User/FetchUserFeedQuery.php new file mode 100644 index 0000000..151635e --- /dev/null +++ b/API/src/Queries/User/FetchUserFeedQuery.php @@ -0,0 +1,26 @@ +searchBackend = $searchBackend; + } + + public function execute(string $userId, int $limit, int $page): array + { + return $this->searchBackend->getUserFeed($userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchUserFeedsQuery.php b/API/src/Queries/User/FetchUserFeedsQuery.php new file mode 100644 index 0000000..3061737 --- /dev/null +++ b/API/src/Queries/User/FetchUserFeedsQuery.php @@ -0,0 +1,26 @@ +searchBackend = $searchBackend; + } + + public function execute(string $userId, int $limit, int $page = 1): array + { + return $this->searchBackend->getUserFeeds($userId, $limit, $page); + } + } +} diff --git a/API/src/Queries/User/FetchUserPasswordQuery.php b/API/src/Queries/User/FetchUserPasswordQuery.php new file mode 100644 index 0000000..7b9b247 --- /dev/null +++ b/API/src/Queries/User/FetchUserPasswordQuery.php @@ -0,0 +1,23 @@ +userService = $userService; + } + + public function execute(string $id) + { + return $this->userService->getPassword($id); + } + } +} diff --git a/API/src/Queries/User/FetchUsernameQuery.php b/API/src/Queries/User/FetchUsernameQuery.php new file mode 100644 index 0000000..8acb181 --- /dev/null +++ b/API/src/Queries/User/FetchUsernameQuery.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(UserId $userId): string + { + return $this->userService->getUsername($userId); + } + } +} diff --git a/API/src/Queries/User/FetchVerificationTokenByEmail.php b/API/src/Queries/User/FetchVerificationTokenByEmail.php new file mode 100644 index 0000000..96efb66 --- /dev/null +++ b/API/src/Queries/User/FetchVerificationTokenByEmail.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(EmailAddress $token) + { + return $this->userService->getVerificationTokenByEmail($token); + } + } +} diff --git a/API/src/Queries/User/FetchVerificationTokenQuery.php b/API/src/Queries/User/FetchVerificationTokenQuery.php new file mode 100644 index 0000000..cb8c4fe --- /dev/null +++ b/API/src/Queries/User/FetchVerificationTokenQuery.php @@ -0,0 +1,27 @@ +userService = $userService; + } + + public function execute(Token $token) + { + return $this->userService->getVerificationToken($token); + } + } +} diff --git a/API/src/Queries/User/IsInvitedQuery.php b/API/src/Queries/User/IsInvitedQuery.php new file mode 100644 index 0000000..c373585 --- /dev/null +++ b/API/src/Queries/User/IsInvitedQuery.php @@ -0,0 +1,27 @@ +betaRequestService = $betaRequestService; + } + + public function execute(EmailAddress $email): bool + { + return $this->betaRequestService->isApproved($email); + } + } +} diff --git a/API/src/Readers/RequestTokenReader.php b/API/src/Readers/RequestTokenReader.php new file mode 100644 index 0000000..fe32b2c --- /dev/null +++ b/API/src/Readers/RequestTokenReader.php @@ -0,0 +1,20 @@ +hasAuthorization()) { + return null; + } + + return $request->getAuthorization()->getBearerToken(); + } + } +} diff --git a/API/src/Routers/EndpointRouter.php b/API/src/Routers/EndpointRouter.php new file mode 100644 index 0000000..2faa657 --- /dev/null +++ b/API/src/Routers/EndpointRouter.php @@ -0,0 +1,67 @@ +accessControl = $accessControl; + } + + public function registerEndpoint(EndpointInterface $endpoint) + { + $this->endpoints[$endpoint->getRequestType()][] = $endpoint; + } + + public function route(RequestInterface $request): ControllerInterface + { + $type = get_class($request); + + if (!isset($this->endpoints[$type])) { + throw new MethodNotAllowed('method not allowed', 'method_not_allowed'); + } + + /** @var EndpointInterface $endpoint */ + foreach ($this->endpoints[$type] as $endpoint) { + if (!$endpoint->canHandle($request)) { + continue; + } + + if (!$this->accessControl->hasAccess($request, $endpoint)) { + throw new Forbidden('access denied', 'access_denied'); + } + + return $endpoint->handle($request); + } + + throw new NotFound('No route found for ' . $request->getUri()->getPath(), 'not_found'); + } + + public function canHandle(RequestInterface $request): bool + { + return true; + } + } +} diff --git a/API/src/Services/BetaRequestService.php b/API/src/Services/BetaRequestService.php new file mode 100644 index 0000000..120430b --- /dev/null +++ b/API/src/Services/BetaRequestService.php @@ -0,0 +1,49 @@ +databaseBackend = $databaseBackend; + } + + public function createBetaRequest(EmailAddress $email): array + { + return $this->databaseBackend->fetch( + 'INSERT INTO beta_requests (email) VALUES (:email) RETURNING *', + [ + 'email' => $email + ] + ); + } + + public function getBetaRequestByEmail(string $email) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM beta_requests WHERE email = :email', + [ + 'email' => $email + ] + ); + } + + public function isApproved(string $email): bool + { + return $this->databaseBackend->fetchColumn('SELECT coalesce(approved, FALSE) FROM beta_requests WHERE email = :email', [ + 'email' => $email + ]); + } + } +} diff --git a/API/src/Services/CollectionService.php b/API/src/Services/CollectionService.php new file mode 100644 index 0000000..4908079 --- /dev/null +++ b/API/src/Services/CollectionService.php @@ -0,0 +1,74 @@ +databaseBackend = $databaseBackend; + } + + public function getCollectionById(CollectionId $collectionId) + { + return $this->databaseBackend->fetch('SELECT * FROM collections WHERE id = :id', [ + 'id' => (string) $collectionId + ]); + } + + public function getCollectionsByUser(UserId $userId, int $limit, int $page = 1): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM collections WHERE owner_id = :id LIMIT :limit OFFSET :offset', + [ + 'id' => (string) $userId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function createCollection(CollectionName $collectionName, UserId $userId): array + { + return $this->databaseBackend->insert( + 'INSERT INTO collections (name, owner_id) VALUES (:name, :owner_id) RETURNING *', + [ + 'name' => (string) $collectionName, + 'owner_id' => (string) $userId + ] + ); + } + + public function updateCollection(CollectionId $collectionId, array $updates) + { + $fields = []; + + foreach ($updates as $field => $_) { + $fields[] = $field . ' = :' . $field; + } + + $updates['id'] = (string) $collectionId; + + $this->databaseBackend->execute('UPDATE collections SET ' . implode(', ', $fields) . ' WHERE id = :id', $updates); + } + + public function deleteCollection(CollectionId $collectionId) + { + $this->databaseBackend->execute('DELETE FROM collections WHERE id = :id', [ + 'id' => (string) $collectionId + ]); + } + } +} diff --git a/API/src/Services/FeedService.php b/API/src/Services/FeedService.php new file mode 100644 index 0000000..6045e4f --- /dev/null +++ b/API/src/Services/FeedService.php @@ -0,0 +1,188 @@ +databaseBackend = $databaseBackend; + } + + public function getFeedById(string $feedId) + { + // TODO: refactor (we can't use public_feeds because of invited feeds) + return $this->databaseBackend->fetch( + 'SELECT feeds.id, + feeds.name, + feeds.description, + feeds.is_verified, + feeds.created, + feeds.updated, + users.id AS owner_id, + users.name AS owner_name, + users.username AS owner_username + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id AND is_owner(feed_users.role) + JOIN users + ON feed_users.user_id = users.id + WHERE feeds.id = :feed_id', + [ + 'feed_id' => $feedId + ] + ); + } + + public function getFeedByIdForUser(string $feedId, string $userId) + { + return $this->databaseBackend->fetch( + 'SELECT feeds.*, + feed_users.user_id IS NOT NULL AS has_added + FROM aggregated_feeds AS feeds + LEFT OUTER JOIN feed_users ON feeds.id = feed_users.feed_id AND feed_users.user_id = :user_id + WHERE feeds.id = :feed_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + + public function getUserFeeds(string $userId, int $limit, int $page = 1): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM user_feeds + WHERE user_id = :user_id + ORDER BY user_feeds.name + LIMIT :limit + OFFSET :offset', + [ + 'user_id' => $userId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function getPublicFeeds(int $limit, int $page = 1) + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM public_feeds + LIMIT :limit + OFFSET :offset', + [ + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function createFeed(string $ownerId, string $name, string $description, bool $isPrivate): array + { + $this->databaseBackend->beginTransaction(); + + try { + $feed = $this->databaseBackend->fetch( + 'INSERT INTO feeds (name, description, is_private) + VALUES (:name, :description, :is_private) + RETURNING *', + [ + 'name' => $name, + 'description' => $description, + 'is_private' => new Boolean($isPrivate) + ] + ); + + $owner = $this->databaseBackend->fetch( + 'INSERT INTO feed_users (feed_id, user_id, role) + VALUES (:feed_id, :user_id, :role) + RETURNING *', + [ + 'feed_id' => $feed['id'], + 'user_id' => $ownerId, + 'role' => (string) new \Timetabio\Library\UserRoles\Owner + ] + ); + } catch (\Exception $exception) { + $this->databaseBackend->rollbackTransaction(); + throw $exception; + } + + $this->databaseBackend->commitTransaction(); + + $feed['owner'] = $owner; + + return $feed; + } + + public function updateFeed(FeedId $feedId, array $updates) + { + $fields = []; + + foreach ($updates as $field => $_) { + $fields[] = $field . ' = :' . $field; + } + + $updates['id'] = $feedId; + + $this->databaseBackend->execute('UPDATE feeds SET ' . implode(', ', $fields) . ' WHERE id = :id', $updates); + } + + public function getVanityByName(string $name) + { + return $this->databaseBackend->fetch('SELECT * FROM feed_vanities WHERE lower(name) = :name', [ + 'name' => mb_strtolower($name) + ]); + } + + public function createFeedVanity(string $feedId, string $vanity): array + { + return $this->databaseBackend->fetch( + 'INSERT INTO feed_vanities (name, feed_id) VALUES (:name, :feed_id) + ON CONFLICT (feed_id) DO UPDATE SET name = :name + RETURNING *', + [ + 'name' => $vanity, + 'feed_id' => $feedId + ] + ); + } + + public function deleteFeedVanity(string $feedId) + { + $this->databaseBackend->execute( + 'DELETE FROM feed_vanities WHERE feed_id = :feed_id', + [ + 'feed_id' => $feedId + ] + ); + } + + public function updateFeedUser(string $feedId, string $userId, UserRole $role) + { + $this->databaseBackend->execute( + 'UPDATE feed_users + SET role = :role + WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId, + 'role' => (string) $role + ] + ); + } + } +} diff --git a/API/src/Services/FileService.php b/API/src/Services/FileService.php new file mode 100644 index 0000000..3e46e4a --- /dev/null +++ b/API/src/Services/FileService.php @@ -0,0 +1,43 @@ +databaseBackend = $databaseBackend; + } + + public function createFile(string $ownerId, string $publicId, string $filename, string $mimeType): array + { + return $this->databaseBackend->fetch( + 'INSERT INTO files (owner_id, public_id, name, mime_type) + VALUES (:owner_id, :public_id, :name, :mime_type) + RETURNING *', + [ + 'owner_id' => $ownerId, + 'public_id' => $publicId, + 'name' => $filename, + 'mime_type' => $mimeType + ] + ); + } + + public function getByPublicId(string $publicId) + { + return $this->databaseBackend->fetch('SELECT * FROM files WHERE public_id = :public_id', [ + 'public_id' => $publicId + ]); + } + } +} diff --git a/API/src/Services/FollowerService.php b/API/src/Services/FollowerService.php new file mode 100644 index 0000000..bf31da5 --- /dev/null +++ b/API/src/Services/FollowerService.php @@ -0,0 +1,59 @@ +databaseBackend = $databaseBackend; + } + + public function getFollower(string $feedId, string $userId) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM feed_users WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'user_id' => $userId, + 'feed_id' => $feedId + ] + ); + } + + public function followFeed(string $feedId, string $userId, string $role) + { + $this->databaseBackend->insert( + 'INSERT INTO feed_users(feed_id, user_id, role) + VALUES(:feed_id, :user_id, :role) + ON CONFLICT(feed_id, user_id) DO NOTHING', + [ + 'feed_id' => $feedId, + 'user_id' => $userId, + 'role' => $role + ] + ); + } + + public function unfollowFeed(string $feedId, string $userId) + { + $this->databaseBackend->insert( + 'DELETE FROM feed_users + WHERE feed_id = :feed_id + AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + } +} diff --git a/API/src/Services/PeopleService.php b/API/src/Services/PeopleService.php new file mode 100644 index 0000000..9bc13b4 --- /dev/null +++ b/API/src/Services/PeopleService.php @@ -0,0 +1,91 @@ +databaseBackend = $databaseBackend; + } + + /** + * @deprecated + */ + public function getPerson(string $feedId, string $userId) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM feed_users WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + + /** + * @deprecated + */ + public function getPeople(string $feedId): array + { + return $this->databaseBackend->fetchAll( + 'SELECT users.name, users.username, feed_users.* + FROM feed_users + JOIN users ON feed_users.user_id = users.id + WHERE feed_id = :feed_id + ORDER BY feed_users.role DESC', + [ + 'feed_id' => $feedId + ] + ); + } + + /** + * @deprecated + */ + public function deletePerson(string $feedId, string $userId) + { + $this->databaseBackend->execute( + 'DELETE FROM feed_users WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + + /** + * @deprecated + */ + public function createPerson(string $feedId, string $userId, bool $post) + { + // TODO: use objects + $role = 'default'; + + if ($post) { + $role = 'moderator'; + } + + $this->databaseBackend->execute( + 'INSERT INTO feed_users(feed_id, user_id, role) VALUES(:feed_id, :user_id, :role)', + [ + 'feed_id' => $feedId, + 'user_id' => $userId, + 'role' => $role + ] + ); + } + } +} diff --git a/API/src/Services/PostService.php b/API/src/Services/PostService.php new file mode 100644 index 0000000..89aca9d --- /dev/null +++ b/API/src/Services/PostService.php @@ -0,0 +1,188 @@ +databaseBackend = $databaseBackend; + } + + public function getPostForUser(string $postId, string $userId) + { + // TODO: move mapping of is_checked to mapper (only return is_checked for tasks) + return $this->databaseBackend->fetch( + 'SELECT post.*, coalesce(annotation.is_checked, FALSE) AS is_checked + FROM aggregated_posts AS post + LEFT OUTER JOIN post_annotations AS annotation + ON post.id = annotation.post_id + AND annotation.user_id = :user_id + WHERE post.id = :post_id', + [ + 'post_id' => $postId, + 'user_id' => $userId + ] + ); + } + + public function getPost(string $postId) + { + // TODO: move mapping of is_checked to mapper (only return is_checked for tasks) + return $this->databaseBackend->fetch( + 'SELECT post.* + FROM aggregated_posts AS post + WHERE post.id = :post_id', + [ + 'post_id' => $postId + ] + ); + } + + public function getPostInfo(string $postId) + { + return $this->databaseBackend->fetch('SELECT id, feed_id, author_id FROM posts WHERE id = :id', [ + 'id' => $postId + ]); + } + + public function getPosts(string $feedId, int $limit, int $page): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * + FROM aggregated_posts AS posts + WHERE posts.feed_id = :feed_id + LIMIT :limit + OFFSET :offset', + [ + 'feed_id' => $feedId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function getPostsForUser(string $feedId, string $userId, int $limit, int $page): array + { + // TODO: move mapping of is_checked to mapper (only return is_checked for tasks) + return $this->databaseBackend->fetchAll( + 'SELECT post.*, coalesce(annotation.is_checked, FALSE) AS is_checked + FROM aggregated_posts AS post + LEFT OUTER JOIN post_annotations AS annotation + ON post.id = annotation.post_id + AND annotation.user_id = :user_id + WHERE post.feed_id = :feed_id + LIMIT :limit + OFFSET :offset', + [ + 'feed_id' => $feedId, + 'user_id' => $userId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function getTodoTasks(string $userId, int $limit, int $page): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM uncompleted_tasks + WHERE user_id = :user_id + LIMIT :limit + OFFSET :offset', + [ + 'user_id' => $userId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function getUpcomingEvents(string $userId, int $limit, int $page): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM upcoming_events + WHERE user_id = :user_id + LIMIT :limit + OFFSET :offset', + [ + 'user_id' => $userId, + 'limit' => $limit, + 'offset' => $limit * ($page - 1) + ] + ); + } + + public function createPost( + PostTypeInterface $type, + string $feedId, + string $authorId, + string $title, + string $body, + Timestamp $timestamp = null): array + { + $stringTimestamp = new \Timetabio\Framework\Pdo\Value\NullValue; + + if ($timestamp !== null) { + $stringTimestamp = (string) $timestamp; + } + + return $this->databaseBackend->insert( + 'INSERT INTO posts (type, feed_id, author_id, title, body, timestamp) + VALUES (:type, :feed_id, :author_id, :title, :body, :timestamp) + RETURNING *', + [ + 'type' => (string) $type, + 'feed_id' => $feedId, + 'author_id' => $authorId, + 'title' => $title, + 'body' => $body, + 'timestamp' => $stringTimestamp + ] + ); + } + + public function createAttachment(string $postId, Attachment $attachment) + { + return $this->databaseBackend->fetch( + 'INSERT INTO post_attachments (post_id, file_id) VALUES (:post_id, :file_id) RETURNING *', + [ + 'post_id' => $postId, + 'file_id' => $attachment->getFileId() + ] + ); + } + + public function getPostAttachments(string $postId) + { + return $this->databaseBackend->fetchAll( + 'SELECT files.public_id, files.name as filename, files.mime_type + FROM post_attachments + JOIN files ON post_attachments.file_id = files.id + WHERE post_attachments.post_id = :post_id', + [ + 'post_id' => $postId + ] + ); + } + + public function deletePost(string $postId) + { + $this->databaseBackend->execute('DELETE FROM posts WHERE id = :id', [ + 'id' => $postId + ]); + } + } +} diff --git a/API/src/Services/UserService.php b/API/src/Services/UserService.php new file mode 100644 index 0000000..4023dda --- /dev/null +++ b/API/src/Services/UserService.php @@ -0,0 +1,172 @@ +databaseBackend = $databaseBackend; + } + + public function getUserById(string $userId) + { + return $this->databaseBackend->fetch('SELECT id, username, is_verified, email, name, created, updated FROM users WHERE id = :id', [ + 'id' => $userId + ]); + } + + public function getUserByEmail(EmailAddress $email) + { + return $this->databaseBackend->fetch('SELECT id, username, is_verified, email, name, created, updated FROM users WHERE email = :email', [ + 'email' => (string) $email + ]); + } + + public function getVerificationToken(string $token) + { + return $this->databaseBackend->fetch('SELECT * FROM verification_tokens WHERE token = :token', [ + 'token' => $token + ]); + } + + public function getVerificationTokenByEmail(string $email) + { + return $this->databaseBackend->fetch( + 'SELECT tokens.user_id, tokens.token FROM verification_tokens as tokens + JOIN users ON tokens.user_id = users.id + WHERE users.email = :email', + [ + 'email' => $email + ] + ); + } + + public function getUserByUsername(string $username) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM users WHERE lower(username) = :username', + [ + 'username' => mb_strtolower($username) + ] + ); + } + + public function getProfile(string $username) + { + return $this->databaseBackend->fetch( + 'SELECT id, username, is_verified, name FROM users WHERE lower(username) = :username', + [ + 'username' => mb_strtolower($username) + ] + ); + } + + public function getLogin(string $user) + { + return $this->databaseBackend->fetch( + 'SELECT id, is_verified, password FROM users + WHERE lower(username) = :username + OR email = :email', + [ + 'email' => $user, + 'username' => mb_strtolower($user) + ] + ); + } + + public function getPassword(string $id) + { + return $this->databaseBackend->fetchColumn( + 'SELECT password FROM users + WHERE id = :id', + [ + 'id' => $id + ] + ); + } + + public function getUsername(string $userId): string + { + $user = $this->databaseBackend->fetch('SELECT username FROM users WHERE id = :id', [ + 'id' => $userId + ]); + + return $user['username']; + } + + public function createUser(EmailAddress $email, Username $username, Password $password, Token $token): array + { + $username = (string) $username; + + $user = [ + 'email' => (string) $email, + 'password' => (string) new Hash($password), + 'username' => $username + ]; + + $inserted = $this->databaseBackend->insert( + 'INSERT INTO users (username, email, password) + VALUES (:username, :email, :password) + RETURNING id', + $user + ); + + $user['id'] = $inserted['id']; + + $this->databaseBackend->insert( + 'INSERT INTO verification_tokens (user_id, token) VALUES (:user_id, :token)', + [ + 'user_id' => $user['id'], + 'token' => (string) $token + ] + ); + + return [ + 'id' => $user['id'] + ]; + } + + public function verifyUser(string $userId) + { + $this->databaseBackend->beginTransaction(); + + $this->databaseBackend->execute('UPDATE users SET is_verified = TRUE WHERE id = :id', [ + 'id' => $userId + ]); + + $this->databaseBackend->execute('DELETE FROM verification_tokens WHERE user_id = :id', [ + 'id' => $userId + ]); + + $this->databaseBackend->commitTransaction(); + } + + public function updateUser(string $userId, array $updates) + { + $fields = []; + + foreach ($updates as $field => $_) { + $fields[] = $field . ' = :' . $field; + } + + $updates['id'] = $userId; + + $this->databaseBackend->execute('UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = :id', $updates); + } + } +} diff --git a/API/src/ValueObjects/AccessToken.php b/API/src/ValueObjects/AccessToken.php new file mode 100644 index 0000000..e7c1ffa --- /dev/null +++ b/API/src/ValueObjects/AccessToken.php @@ -0,0 +1,92 @@ +token = $token; + $this->accessType = $accessType; + $this->userId = $userId; + $this->autoRenew = $autoRenew; + $this->expires = $expires; + } + + public function getToken(): Token + { + return $this->token; + } + + public function getAccessType(): AccessTypeInterface + { + return $this->accessType; + } + + public function hasUserId(): bool + { + return $this->userId !== null; + } + + public function getUserId(): UserId + { + return $this->userId; + } + + public function getExpires(): int + { + return $this->expires; + } + + public function getAutoRenew(): bool + { + return $this->autoRenew; + } + + public function jsonSerialize(): array + { + return [ + 'token' => (string) $this->token, + 'userId' => $this->userId, + 'expires' => $this->expires, + 'auto_renew' => $this->autoRenew + ]; + } + } +} diff --git a/API/src/ValueObjects/Attachment.php b/API/src/ValueObjects/Attachment.php new file mode 100644 index 0000000..b01fbb7 --- /dev/null +++ b/API/src/ValueObjects/Attachment.php @@ -0,0 +1,39 @@ +publicId = $filename; + } + + public function getPublicId(): string + { + return $this->publicId; + } + + public function getFileId(): string + { + return $this->fileId; + } + + public function setFileId(string $fileId) + { + $this->fileId = $fileId; + } + } +} diff --git a/API/src/ValueObjects/BsonDateTime.php b/API/src/ValueObjects/BsonDateTime.php new file mode 100644 index 0000000..08705fa --- /dev/null +++ b/API/src/ValueObjects/BsonDateTime.php @@ -0,0 +1,30 @@ +value = new UTCDateTime($time * 1000); + } + + public function getValue(): UTCDateTime + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/CollectionId.php b/API/src/ValueObjects/CollectionId.php new file mode 100644 index 0000000..b40115b --- /dev/null +++ b/API/src/ValueObjects/CollectionId.php @@ -0,0 +1,19 @@ += 40) { + throw new \Exception('collection name must be between 1 and 40 characters long'); + } + + // TODO: Maybe regex check? + + $this->name = $name; + } + + public function __toString(): string + { + return $this->name; + } + } +} diff --git a/API/src/ValueObjects/FeedDescription.php b/API/src/ValueObjects/FeedDescription.php new file mode 100644 index 0000000..f761eb5 --- /dev/null +++ b/API/src/ValueObjects/FeedDescription.php @@ -0,0 +1,35 @@ + 140) { + throw new \Exception('feed description limit of 140 b exceeded'); + } + + $this->value = $trimmed; + } + + public function __toString(): string + { + return $this->value; + } + + public function jsonSerialize(): string + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/FeedFile.php b/API/src/ValueObjects/FeedFile.php new file mode 100644 index 0000000..ce16dee --- /dev/null +++ b/API/src/ValueObjects/FeedFile.php @@ -0,0 +1,48 @@ +filename = $this->parse($filename); + $this->publicId = new FileToken; + } + + private function parse(string $filename): string + { + $info = pathinfo($filename); + + return $info['basename']; + } + + public function getPublicId(): FileToken + { + return $this->publicId; + } + + public function getFilename(): string + { + return $this->filename; + } + + public function __toString(): string + { + return $this->publicId . '/' . $this->filename; + } + } +} diff --git a/API/src/ValueObjects/FeedId.php b/API/src/ValueObjects/FeedId.php new file mode 100644 index 0000000..8e2ef6f --- /dev/null +++ b/API/src/ValueObjects/FeedId.php @@ -0,0 +1,22 @@ + 64) { + throw new \Exception('feed name limit of 64 b exceeded'); + } + + $this->value = $trimmed; + } + + public function __toString(): string + { + return $this->value; + } + + public function jsonSerialize(): string + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/FeedVanity.php b/API/src/ValueObjects/FeedVanity.php new file mode 100644 index 0000000..1ec7e40 --- /dev/null +++ b/API/src/ValueObjects/FeedVanity.php @@ -0,0 +1,11 @@ +hash = password_hash((string) $password, PASSWORD_DEFAULT); + } + + public function __toString(): string + { + return $this->hash; + } + } +} diff --git a/API/src/ValueObjects/Pagination.php b/API/src/ValueObjects/Pagination.php new file mode 100644 index 0000000..f713754 --- /dev/null +++ b/API/src/ValueObjects/Pagination.php @@ -0,0 +1,52 @@ +limit = $limit; + $this->page = $page; + $this->total = $total; + $this->results = $results; + } + + public function jsonSerialize(): array + { + return [ + 'filter' => [ + 'limit' => $this->limit, + 'page' => $this->page, + ], + 'meta' => [ + 'total' => $this->total, + 'pages' => ceil($this->total / $this->limit) + ], + 'results' => $this->results + ]; + } + } +} diff --git a/API/src/ValueObjects/Password.php b/API/src/ValueObjects/Password.php new file mode 100644 index 0000000..447ba89 --- /dev/null +++ b/API/src/ValueObjects/Password.php @@ -0,0 +1,35 @@ + 72) { + throw new \Exception('password must be between 8 and 72 characters'); + } + + $this->password = $password; + } + + public function verify(string $hash): bool + { + return password_verify($this->password, $hash); + } + + public function __toString(): string + { + return $this->password; + } + } +} diff --git a/API/src/ValueObjects/PostBody.php b/API/src/ValueObjects/PostBody.php new file mode 100644 index 0000000..aacd97b --- /dev/null +++ b/API/src/ValueObjects/PostBody.php @@ -0,0 +1,29 @@ + 8192) { + throw new \Exception('post max size exceeded'); + } + + $this->value = $value; + } + + public function __toString(): string + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/PostTitle.php b/API/src/ValueObjects/PostTitle.php new file mode 100644 index 0000000..b1ac650 --- /dev/null +++ b/API/src/ValueObjects/PostTitle.php @@ -0,0 +1,34 @@ + 64) { + throw new \Exception('post title limit of 64 b exceeded'); + } + + $this->value = $trimmed; + } + + public function __toString(): string + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/StringBoolean.php b/API/src/ValueObjects/StringBoolean.php new file mode 100644 index 0000000..224c28e --- /dev/null +++ b/API/src/ValueObjects/StringBoolean.php @@ -0,0 +1,36 @@ +value = $this->parseValue($value); + } + + private function parseValue(string $value): bool + { + switch ($value) { + case 'true': + return true; + case 'false': + return false; + } + + throw new \Exception('invalid value'); + } + + public function getValue(): bool + { + return $this->value; + } + } +} diff --git a/API/src/ValueObjects/UploadParams.php b/API/src/ValueObjects/UploadParams.php new file mode 100644 index 0000000..f1be369 --- /dev/null +++ b/API/src/ValueObjects/UploadParams.php @@ -0,0 +1,46 @@ +file = $file; + $this->endpoint = $endpoint; + $this->params = $params; + } + + public function getFile(): FeedFile + { + return $this->file; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function getParams(): array + { + return $this->params; + } + } +} diff --git a/API/src/ValueObjects/UserId.php b/API/src/ValueObjects/UserId.php new file mode 100644 index 0000000..712b4d1 --- /dev/null +++ b/API/src/ValueObjects/UserId.php @@ -0,0 +1,19 @@ + 20) { + throw new \Exception('username must be between 3 and 20 characters long'); + } + + if (!preg_match('/^[\w-]+$/u', $username)) { + throw new \Exception('invalid username'); + } + + $this->username = $username; + } + + public function __toString(): string + { + return $this->username; + } + + public function jsonSerialize(): string + { + return $this->username; + } + } +} diff --git a/API/tests/bootstrap.php b/API/tests/bootstrap.php new file mode 100644 index 0000000..b41761f --- /dev/null +++ b/API/tests/bootstrap.php @@ -0,0 +1,10 @@ + JS_FILES do |t| + mkdir_p 'build' + sh 'rollup', '-c', ROLLUP_CONFIG, '-o', t.name, 'js/application.js' +end + +desc 'Builds the polyfills bundle' +file 'build/polyfills.js' => POLYFILLS do |t| + mkdir_p 'build' + sh 'uglifyjs', '-o', t.name, *t.prerequisites +end diff --git a/Application/js/_stubs/dom4.js b/Application/js/_stubs/dom4.js new file mode 100644 index 0000000..fbec147 --- /dev/null +++ b/Application/js/_stubs/dom4.js @@ -0,0 +1,18 @@ +/** + * + * @returns void + */ +HTMLElement.prototype.remove = function () { +} + +/** + * @typedef {{capture?: boolean, once?: boolean, passive?: boolean}} EventListenerOptions + */ + +/** + @param {string} type + @param {EventListener|Function} listener + @param {EventListenerOptions|boolean} [options] + */ +EventTarget.prototype.addEventListener = function (type, listener, options = false) { +} diff --git a/Application/js/_stubs/encoding.js b/Application/js/_stubs/encoding.js new file mode 100644 index 0000000..482b0fe --- /dev/null +++ b/Application/js/_stubs/encoding.js @@ -0,0 +1,11 @@ +class TextEncoder { + /** + * + * @param {string} buffer + * @param {{stream: boolean}} [options] + * + * @returns {Uint8Array} + */ + encode(buffer, options = {}) { + } +} diff --git a/Application/js/_stubs/events.js b/Application/js/_stubs/events.js new file mode 100644 index 0000000..46d88d1 --- /dev/null +++ b/Application/js/_stubs/events.js @@ -0,0 +1,23 @@ +/** + * + * @extends {MouseEvent} + * @extends {Event} + */ +function DragEvent () {} + +/** + * + * @type {DataTransfer} + */ +DragEvent.prototype.dataTransfer = null; + +/** + * @typedef {{bubbles?: boolean, cancelable?: boolean, scoped?: boolean, composed?: boolean, detail?: {}}} CustomEventInit + */ + +/** + @param {string} type + @param {CustomEventInit} [eventInitDict] + @constructor + */ +function CustomEvent(type,eventInitDict) {} diff --git a/Application/js/_stubs/object.js b/Application/js/_stubs/object.js new file mode 100644 index 0000000..d190811 --- /dev/null +++ b/Application/js/_stubs/object.js @@ -0,0 +1,7 @@ +/** + * + * @param {{}} object + * @returns {Array>} + */ +Object.entries = function (object) { +} diff --git a/Application/js/app/ajax.js b/Application/js/app/ajax.js new file mode 100644 index 0000000..c69b42b --- /dev/null +++ b/Application/js/app/ajax.js @@ -0,0 +1,77 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { createToastMessage } from '../dom/toast' + +/** + * + * @returns {Promise} + */ +export function handleAjaxError () { + return createToastMessage('Oops. Something went wrong. Please retry a little later.', { ttl: 4000 }) + .show() +} + +/** + * + * @param {{}} data + * @param {(function(string):boolean)} [errorFn] + * @returns {Promise|null} + */ +export function handleAjaxResponse (data, errorFn = () => false) { + Object.keys(data) + .forEach((key) => { + const value = data[ key ] + + switch (key) { + case 'error': + return handleError(value, errorFn) + case 'redirect': + return handleRedirect(value) + case 'reload': + return handleReload() + case 'toast': + return handleToast(value) + } + }) +} + +/** + * + * @param {string} error + * @param {(function(string):boolean)} errorFn + * @returns {Promise|null} + */ +function handleError (error, errorFn) { + const result = errorFn(error) + + if (result) { + return null + } + + return createToastMessage(error, { ttl: 4000, error: true }) + .show() +} + +/** + * + * @param {string} value + */ +function handleRedirect (value) { + window.location.href = value +} + +/** + * + * @param {{message: string, ttl?: number}} toast + * @returns {Promise} + */ +function handleToast (toast) { + return createToastMessage(toast.message, { ttl: toast.ttl }) + .show() +} + +function handleReload () { + window.location.reload() +} diff --git a/Application/js/application.js b/Application/js/application.js new file mode 100644 index 0000000..2505f0f --- /dev/null +++ b/Application/js/application.js @@ -0,0 +1,6 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import './bootstrap/browser' +import './bootstrap/elements' diff --git a/Application/js/bootstrap/browser.js b/Application/js/bootstrap/browser.js new file mode 100644 index 0000000..ad36c67 --- /dev/null +++ b/Application/js/bootstrap/browser.js @@ -0,0 +1,7 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +if (/MSIE|Trident/.test(navigator.userAgent)) { + document.getElementById('outdated-browser').removeAttribute('hidden') +} diff --git a/Application/js/bootstrap/elements.js b/Application/js/bootstrap/elements.js new file mode 100644 index 0000000..84115e2 --- /dev/null +++ b/Application/js/bootstrap/elements.js @@ -0,0 +1,42 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { AjaxForm } from '../elements/ajax-form' +import { AjaxSelect } from '../elements/ajax-select' +import { FormError } from '../elements/form-error' +import { AutoTextarea } from '../elements/auto-textarea' +import { FollowButton } from '../elements/follow-button' +import { AjaxButton } from '../elements/ajax-button' +import { FileDrop } from '../elements/file-drop' +import { FileUpload } from '../elements/file-upload' +import { PostAttachment } from '../elements/post-attachment' +import { FilePick } from '../elements/file-pick' +import { ValidatedInput } from '../elements/validated-input' +import { LocalTimeElement, TimeAgoElement } from '../elements/time-elements' +import { PaginatedView } from '../elements/paginated-view' +import { PaginatedList } from '../elements/paginated-list' +import { PaginationButton } from '../elements/pagination-button' +import { ToastMessage } from '../elements/toast-message' + +window.customElements.define('ajax-form', AjaxForm, { extends: 'form' }) +window.customElements.define('ajax-select', AjaxSelect, { extends: 'select' }) +window.customElements.define('form-error', FormError) +window.customElements.define('auto-textarea', AutoTextarea, { extends: 'textarea' }) +window.customElements.define('follow-button', FollowButton, { extends: 'button' }) +window.customElements.define('ajax-button', AjaxButton, { extends: 'button' }) +window.customElements.define('file-drop', FileDrop) +window.customElements.define('file-pick', FilePick) +window.customElements.define('validated-input', ValidatedInput, { extends: 'input' }) +window.customElements.define('toast-message', ToastMessage) + +// pagination +window.customElements.define('paginated-view', PaginatedView) +window.customElements.define('paginated-list', PaginatedList) +window.customElements.define('pagination-button', PaginationButton, { extends: 'button' }) + +window.customElements.define('file-upload', FileUpload) +window.customElements.define('post-attachment', PostAttachment) + +window.customElements.define('time-ago', TimeAgoElement) +window.customElements.define('local-time', LocalTimeElement) diff --git a/Application/js/dom/custom-events.js b/Application/js/dom/custom-events.js new file mode 100644 index 0000000..775800e --- /dev/null +++ b/Application/js/dom/custom-events.js @@ -0,0 +1,9 @@ +/** +* (c) 2016 Ruben Schmidmeister +*/ + +export const EventName = { + nextPage: 'nextPage', + filesAdded: 'filesAdded', + formValidityChange: 'formValidityChange' +} diff --git a/Application/js/dom/environment.js b/Application/js/dom/environment.js new file mode 100644 index 0000000..6274bfe --- /dev/null +++ b/Application/js/dom/environment.js @@ -0,0 +1,13 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * + * @returns {string} + */ +export function getCsrfToken () { + return document + .querySelector('meta[name="csrf-token"]') + .getAttribute('content') +} diff --git a/Application/js/dom/fetch.js b/Application/js/dom/fetch.js new file mode 100644 index 0000000..3ab094b --- /dev/null +++ b/Application/js/dom/fetch.js @@ -0,0 +1,18 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * + * @param {Response} response + */ +export function rejectHttpErrors(response) { + const status = response.status + + if (status >= 200 && status < 400) { + return response + } + + return Promise.reject(new Error(`http status code ${status} is an error`)) +} + diff --git a/Application/js/dom/form.js b/Application/js/dom/form.js new file mode 100644 index 0000000..52a0614 --- /dev/null +++ b/Application/js/dom/form.js @@ -0,0 +1,17 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * + * @param {{}} data + * @returns {FormData} + */ +export function formData (data) { + const formData = new FormData() + + Object.entries(data) + .forEach((entry) => formData.append(...entry)) + + return formData +} diff --git a/Application/js/dom/next-render.js b/Application/js/dom/next-render.js new file mode 100644 index 0000000..c9e44b7 --- /dev/null +++ b/Application/js/dom/next-render.js @@ -0,0 +1,32 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +let waitingNextRender = false +let afterNextRenderQueue = [] + +/** + * + * Calls the given function after the next render has occurred. + * + * @param {Function} callbackFn + */ +export function defer (callbackFn) { + watchNextRender() + afterNextRenderQueue.push(callbackFn) +} + +function watchNextRender () { + if (!waitingNextRender) { + waitingNextRender = true + + const fn = () => { + afterNextRenderQueue.forEach((callbackFn) => callbackFn()) + afterNextRenderQueue = [] + waitingNextRender = false + } + + // noinspection JSCheckFunctionSignatures + window.requestAnimationFrame(() => setTimeout(fn)) + } +} diff --git a/Application/js/dom/string.js b/Application/js/dom/string.js new file mode 100644 index 0000000..dd620f7 --- /dev/null +++ b/Application/js/dom/string.js @@ -0,0 +1,16 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * + * @param {string} string + * @returns {number} + */ +export function getByteSize(string) { + if (window.TextEncoder) { + return (new TextEncoder()).encode(string).length + } + + return string.length +} diff --git a/Application/js/dom/toast.js b/Application/js/dom/toast.js new file mode 100644 index 0000000..7935bbc --- /dev/null +++ b/Application/js/dom/toast.js @@ -0,0 +1,20 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { ToastMessage } from '../elements/toast-message' + +/** + * + * @param {string} message + * @param {number} ttl + * @returns {ToastMessage} + */ +export function createToastMessage(message, { ttl = 3000 } = {}) { + const $message = new ToastMessage() + + $message.innerText = message + $message.toastTtl = ttl + + return $message +} diff --git a/Application/js/dom/uploader.js b/Application/js/dom/uploader.js new file mode 100644 index 0000000..bbfd443 --- /dev/null +++ b/Application/js/dom/uploader.js @@ -0,0 +1,56 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { Signal } from '../event/signal' + +export class Uploader { + constructor () { + /** + * + * @type {Signal} + */ + this.onProgress = new Signal() + + /** + * + * @type {XMLHttpRequest} + * @private + */ + this._xhr = null + } + + cancel () { + if (this._xhr) { + this._xhr.abort() + } + } + + /** + * + * @param {string} url + * @param {FormData} data + * @returns {Promise} + */ + execute (url, data) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + + this._xhr = xhr + + xhr.addEventListener('load', () => { + resolve() + }) + + xhr.upload.addEventListener('progress', (event) => { + this.onProgress.dispatch({ loaded: event.loaded, total: event.total }) + }) + + xhr.addEventListener('abort', () => { + reject() + }) + + xhr.open('POST', url) + xhr.send(data) + }) + } +} diff --git a/Application/js/elements/ajax-button.js b/Application/js/elements/ajax-button.js new file mode 100644 index 0000000..9d15358 --- /dev/null +++ b/Application/js/elements/ajax-button.js @@ -0,0 +1,70 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { getCsrfToken } from '../dom/environment' +import { rejectHttpErrors } from '../dom/fetch' +import { handleAjaxError, handleAjaxResponse } from '../app/ajax' + +export class AjaxButton extends HTMLButtonElement { + + constructor () { + super() + this._onClick = this._onClick.bind(this) + } + + connectedCallback () { + this.addEventListener('click', this._onClick) + } + + disconnectedCallback () { + this.removeEventListener('click', this._onClick) + } + + _onClick () { + this._fetch() + .then(rejectHttpErrors) + .then((resp) => resp.json()) + .then((data) => handleAjaxResponse(data)) + .catch(() => handleAjaxError()) + } + + /** + * + * @returns {Promise} + * @private + */ + _fetch () { + const postData = this.postData + const data = new window.FormData() + + Object.entries(postData) + .forEach(([key, value]) => { + data.append(key, value) + }) + + data.append('token', getCsrfToken()) + + return window.fetch(this.postUri, { + method: 'post', + credentials: 'same-origin', + body: data + }) + } + + /** + * + * @returns {string} + */ + get postUri () { + return this.getAttribute('post-uri') + } + + /** + * + * @returns {{}} + */ + get postData () { + return JSON.parse(this.getAttribute('post-data')) + } +} diff --git a/Application/js/elements/ajax-form.js b/Application/js/elements/ajax-form.js new file mode 100644 index 0000000..e4a6e87 --- /dev/null +++ b/Application/js/elements/ajax-form.js @@ -0,0 +1,125 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { getCsrfToken } from '../dom/environment' +import { EventName } from '../dom/custom-events' +import { rejectHttpErrors } from '../dom/fetch' +import { handleAjaxError, handleAjaxResponse } from '../app/ajax' + +export class AjaxForm extends window.HTMLFormElement { + constructor () { + super() + + this._onSubmit = this._onSubmit.bind(this) + this._onInput = this._onInput.bind(this) + this._onChange = this._onChange.bind(this) + this._onValidityChange = this._onValidityChange.bind(this) + this._onFilesAdded = this._onFilesAdded.bind(this) + this._onError = this._onError.bind(this) + } + + connectedCallback () { + this.addEventListener('submit', this._onSubmit) + this.addEventListener('input', this._onInput) + this.addEventListener('change', this._onChange) + this.addEventListener(EventName.formValidityChange, this._onValidityChange) + this.addEventListener(EventName.filesAdded, this._onFilesAdded) + } + + disconnectedCallback () { + this.removeEventListener('submit', this._onSubmit) + this.removeEventListener('input', this._onInput) + this.removeEventListener('change', this._onChange) + this.removeEventListener(EventName.formValidityChange, this._onValidityChange) + this.removeEventListener(EventName.filesAdded, this._onFilesAdded) + } + + _onSubmit (event) { + event.preventDefault() + + this._submit() + } + + _onValidityChange () { + this._validate() + } + + _onChange () { + // Well... Thank you chrome for not firing 'input' events for changing radio buttons. + // Love ya chrome, you're the sweetest + this._validate() + } + + _onInput () { + this._validate() + } + + _validate () { + const submitButton = this.querySelector('[type="submit"]') + const isValid = this.checkValidity() + + submitButton.disabled = !isValid + } + + /** + * + * @param {CustomEvent} event + * @private + */ + _onFilesAdded (event) { + event.stopPropagation() + + this.querySelector('file-drop').addFiles(event.detail.files) + } + + /** + * + * @returns {Promise} + * @private + */ + _submit () { + const data = new window.FormData(this) + + data.append('token', getCsrfToken()) + + const params = { + method: this.method, + credentials: 'same-origin', + body: data + } + + return window.fetch(this.action, params) + .then(rejectHttpErrors) + .then((resp) => resp.json()) + .then((data) => handleAjaxResponse(data, this._onError)) + .catch(() => handleAjaxError()) + } + + /** + * + * @param {string} error + * @returns {boolean} + * @private + */ + _onError (error) { + const $error = this.$error + + if (!$error) { + return false + } + + $error.error = error + + return true + } + + /** + * + * @returns {FormError} + */ + get $error () { + // noinspection JSValidateTypes + return this.querySelector('form-error') + } +} diff --git a/Application/js/elements/ajax-select.js b/Application/js/elements/ajax-select.js new file mode 100644 index 0000000..96362b8 --- /dev/null +++ b/Application/js/elements/ajax-select.js @@ -0,0 +1,72 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { getCsrfToken } from '../dom/environment' +import { rejectHttpErrors } from '../dom/fetch' +import { handleAjaxError, handleAjaxResponse } from '../app/ajax' + + +export class AjaxSelect extends HTMLSelectElement { + + constructor () { + super() + this._onChange = this._onChange.bind(this) + } + + connectedCallback () { + this.addEventListener('change', this._onChange) + } + + disconnectedCallback () { + this.removeEventListener('change', this._onChange) + } + + _onChange () { + this._fetch() + .then(rejectHttpErrors) + .then((resp) => resp.json()) + .then((data) => handleAjaxResponse(data)) + .catch(() => handleAjaxError()) + } + + /** + * + * @returns {Promise} + * @private + */ + _fetch () { + const postData = this.postData + const data = new window.FormData() + + Object.entries(postData) + .forEach(([key, value]) => { + data.append(key, value) + }) + + data.append('token', getCsrfToken()) + data.append(this.name, this.value) + + return window.fetch(this.postUri, { + method: 'post', + credentials: 'same-origin', + body: data + }) + } + + /** + * + * @returns {string} + */ + get postUri () { + return this.getAttribute('post-uri') + } + + /** + * + * @returns {{}} + */ + get postData () { + return JSON.parse(this.getAttribute('post-data')) + } +} diff --git a/Application/js/elements/auto-textarea.js b/Application/js/elements/auto-textarea.js new file mode 100644 index 0000000..a0e59f6 --- /dev/null +++ b/Application/js/elements/auto-textarea.js @@ -0,0 +1,45 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { defer } from '../dom/next-render.js' + +export class AutoTextarea extends window.HTMLTextAreaElement { + constructor () { + super() + + this._resize = this._resize.bind(this) + this._validate = this._validate.bind(this) + + defer(() => this._resize()) + } + + connectedCallback () { + this.addEventListener('input', this._resize) + this.addEventListener('input', this._validate) + } + + disconnectedCallback () { + this.removeEventListener('input', this._resize) + this.removeEventListener('input', this._validate) + } + + _resize () { + this.style.height = 'auto' + this.style.height = `${this.scrollHeight}px` + } + + _validate () { + const isValid = (this.value.length < this.maxSize) + let message = '' + + if (!isValid) { + message = 'Max size reached' + } + + this.setCustomValidity(message) + } + + get maxSize () { + return Number.parseInt(this.getAttribute('max-size')) + } +} diff --git a/Application/js/elements/file-drop.js b/Application/js/elements/file-drop.js new file mode 100644 index 0000000..afe187a --- /dev/null +++ b/Application/js/elements/file-drop.js @@ -0,0 +1,107 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +export class FileDrop extends HTMLElement { + + constructor () { + super() + + this._onDrop = this._onDrop.bind(this) + this._onDragOver = this._onDragOver.bind(this) + this._onDragEnter = this._onDragEnter.bind(this) + this._onDragLeave = this._onDragLeave.bind(this) + this._onDragLeave = this._onDragLeave.bind(this) + } + + connectedCallback () { + this.addEventListener('drop', this._onDrop) + this.addEventListener('dragover', this._onDragOver) + this.addEventListener('dragenter', this._onDragEnter) + this.addEventListener('dragleave', this._onDragLeave) + } + + disconnectedCallback () { + this.removeEventListener('drop', this._onDrop) + this.removeEventListener('dragover', this._onDragOver) + this.removeEventListener('dragenter', this._onDragEnter) + this.removeEventListener('dragleave', this._onDragLeave) + } + + /** + * + * @param {DragEvent} event + * @private + */ + _onDrop (event) { + event.preventDefault() + + this.classList.remove('-drag-over') + + this.addFiles(event.dataTransfer.files) + } + + /** + * + * @param {FileList} files + */ + addFiles (files) { + const $appendTo = this.querySelector(this.appendTo) + + Array.from(files).forEach((file) => { + const $file = document.createElement(this.fileElement) + + $file.setFile(file) + $file.upload() + + $appendTo.appendChild($file) + }) + } + + /** + * + * @param {DragEvent} event + * @private + */ + _onDragOver (event) { + event.preventDefault() + + this.classList.add('-drag-over') + } + + /** + * + * @param {DragEvent} event + * @private + */ + _onDragEnter (event) { + event.preventDefault() + } + + /** + * + * @param {DragEvent} event + * @private + */ + _onDragLeave (event) { + event.preventDefault() + + this.classList.remove('-drag-over') + } + + /** + * + * @returns {string} + */ + get appendTo () { + return this.getAttribute('append-to') + } + + /** + * + * @returns {string} + */ + get fileElement () { + return this.getAttribute('file-element') + } +} diff --git a/Application/js/elements/file-pick.js b/Application/js/elements/file-pick.js new file mode 100644 index 0000000..45e9e4c --- /dev/null +++ b/Application/js/elements/file-pick.js @@ -0,0 +1,31 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { EventName } from '../dom/custom-events' + +export class FilePick extends HTMLElement { + + constructor () { + super() + } + + connectedCallback () { + const $input = document.createElement('input') + + $input.type = 'file' + $input.hidden = true + $input.multiple = true + + $input.addEventListener('change', () => { + this.dispatchEvent(new CustomEvent(EventName.filesAdded, { detail: { files: $input.files }, bubbles: true })) + }) + + this.addEventListener('click', () => { + $input.click() + }) + + this.appendChild($input) + } + +} diff --git a/Application/js/elements/file-upload.js b/Application/js/elements/file-upload.js new file mode 100644 index 0000000..9ebe23a --- /dev/null +++ b/Application/js/elements/file-upload.js @@ -0,0 +1,123 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { getCsrfToken } from '../dom/environment' +import { formData } from '../dom/form' +import { Uploader } from '../dom/uploader' + +/** + * + * @param {*} params + * @returns {Promise<*>} + */ +function jsonFetch (...params) { + return fetch(...params) + .then((resp) => resp.json()) +} + +/** + * + * @param {File} file + * @returns {Promise} + */ +function createUpload (file) { + return jsonFetch('/action/upload', { + method: 'POST', + credentials: 'same-origin', + body: formData({ + filename: file.name, + mime_type: file.type, + token: getCsrfToken() + }) + }) +} + +export class FileUpload extends HTMLElement { + + constructor () { + super() + + this._onProgress = this._onProgress.bind(this) + } + + connectedCallback () { + } + + disconnectedCallback () { + } + + /** + * + * @param {File} file + */ + setFile (file) { + this._file = file + this._render() + } + + /** + * @returns {Promise} + */ + upload () { + this._renderUpload() + this._renderInput() + + const uploader = new Uploader() + uploader.onProgress.addListener(this._onProgress) + + const credentials = createUpload(this._file) + const upload = credentials.then((data) => { + const form = formData(Object.assign(data.params, { file: this._file })) + + return uploader.execute(data.endpoint, form) + }) + + upload.then(() => { + uploader.onProgress.removeListener(this._onProgress) + this.classList.add('-uploaded') + }) + + Promise.all([ credentials, upload ]) + .then(([data]) => { + this._onDone(data) + }) + + return upload + } + + /** + * + * @param {{loaded: number, total: number}} progress + * @private + */ + _onProgress (progress) { + } + + /** + * + * @private + */ + _onDone () { + } + + /** + * + * @private + */ + _renderInput () { + } + + /** + * + * @private + */ + _render () { + } + + /** + * + * @private + */ + _renderUpload () { + } +} diff --git a/Application/js/elements/follow-button.js b/Application/js/elements/follow-button.js new file mode 100644 index 0000000..cd17804 --- /dev/null +++ b/Application/js/elements/follow-button.js @@ -0,0 +1,91 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { getCsrfToken } from '../dom/environment' + +export class FollowButton extends window.HTMLButtonElement { + constructor () { + super() + this._onClick = this._onClick.bind(this) + } + + connectedCallback () { + this.addEventListener('click', this._onClick) + } + + disconnectedCallback () { + this.removeEventListener('click', this._onClick) + } + + _onClick (event) { + this._fetch() + .then((resp) => resp.json()) + .then((data) => { + this.isFollowing = data.following + this.innerText = this._getLabel() + }) + } + + _fetch () { + const data = new window.FormData() + + data.append('token', getCsrfToken()) + data.append('feed_id', this.feedId) + + return window.fetch(this._getEndpoint(), { + method: 'post', + credentials: 'same-origin', + body: data + }) + } + + /** + * + * @returns {string} + * @private + */ + _getEndpoint () { + return this.isFollowing + ? '/action/unfollow' + : '/action/follow' + } + + /** + * + * @returns {string} + * @private + */ + _getLabel () { + return this.isFollowing + ? this.getAttribute('unfollow-label') + : this.getAttribute('follow-label') + } + + /** + * + * @returns {boolean} + */ + get isFollowing () { + return this.hasAttribute('is-following') + } + + /** + * + * @param {boolean} _isFollowing + */ + set isFollowing (_isFollowing) { + if (_isFollowing) { + this.setAttribute('is-following', '') + } else { + this.removeAttribute('is-following') + } + } + + /** + * + * @returns {string} + */ + get feedId () { + return this.getAttribute('feed-id') + } +} diff --git a/Application/js/elements/form-error.js b/Application/js/elements/form-error.js new file mode 100644 index 0000000..34b1ab5 --- /dev/null +++ b/Application/js/elements/form-error.js @@ -0,0 +1,36 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +export class FormError extends window.HTMLElement { + set error (error) { + let empty = false + + if (error == null) { + empty = true + } + + this.innerText = error || '' + this.empty = empty + } + + /** + * + * @param {boolean} empty + */ + set empty (empty) { + if (empty) { + this.setAttribute('empty', '') + } else { + this.removeAttribute('empty') + } + } + + /** + * + * @returns {boolean} + */ + get empty () { + return this.hasAttribute('empty') + } +} diff --git a/Application/js/elements/form-field.js b/Application/js/elements/form-field.js new file mode 100644 index 0000000..56239ba --- /dev/null +++ b/Application/js/elements/form-field.js @@ -0,0 +1,7 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +export class FormField extends window.HTMLLabelElement { + +} diff --git a/Application/js/elements/paginated-list.js b/Application/js/elements/paginated-list.js new file mode 100644 index 0000000..a9170ee --- /dev/null +++ b/Application/js/elements/paginated-list.js @@ -0,0 +1,11 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +export class PaginatedList extends HTMLElement { + connectedCallback () { + } + + disconnectedCallback () { + } +} diff --git a/Application/js/elements/paginated-view.js b/Application/js/elements/paginated-view.js new file mode 100644 index 0000000..f5019b7 --- /dev/null +++ b/Application/js/elements/paginated-view.js @@ -0,0 +1,120 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { EventName } from '../dom/custom-events' + +/** + * + * @param {string} html + * @returns {DocumentFragment} + */ +function parseHtml (html) { + const range = document.createRange() + + range.selectNode(document.body) + + return range.createContextualFragment(html) +} + +export const State = { + idle: 0, + loading: 1, + complete: 2 +} + +export class PaginatedView extends HTMLElement { + constructor () { + super() + + this._page = 1 + + this._onNextPage = this._onNextPage.bind(this) + } + + connectedCallback () { + this.addEventListener(EventName.nextPage, this._onNextPage) + this._setState(State.idle) + } + + disconnectedCallback () { + this.removeEventListener(EventName.nextPage, this._onNextPage) + } + + _onNextPage () { + if (this._state !== State.idle) { + return Promise.resolve() + } + + this._setState(State.loading) + + return fetch(`${this.endpointUri}?page=${this._page + 1}`, { credentials: 'same-origin' }) + .then((resp) => resp.text()) + .then((body) => parseHtml(body)) + .then((result) => this._onResponse(result)) + } + + /** + * + * @param {DocumentFragment} fragment + * @private + */ + _onResponse (fragment) { + this._page += 1 + + let state = State.idle + + if (this._page >= this.totalPages) { + state = State.complete + } + + this.$list.appendChild(fragment) + + this._setState(state) + } + + /** + * + * @param {number} state + * @private + */ + _setState (state) { + this._state = state + + if (this.$button) { + this.$button.setState(state) + } + } + + /** + * + * @returns {string} + */ + get endpointUri () { + return this.getAttribute('endpoint-uri') + } + + /** + * + * @returns {number} + */ + get totalPages () { + return Number.parseInt(this.getAttribute('total-pages')) + } + + /** + * + * @returns {Element} + */ + get $list () { + return this.querySelector('paginated-list') + } + + /** + * + * @returns {Element} + */ + get $button () { + return this.querySelector('button[is="pagination-button"]') + } +} diff --git a/Application/js/elements/pagination-button.js b/Application/js/elements/pagination-button.js new file mode 100644 index 0000000..eca2033 --- /dev/null +++ b/Application/js/elements/pagination-button.js @@ -0,0 +1,62 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { EventName } from '../dom/custom-events' +import { State } from './paginated-view' + +export class PaginationButton extends HTMLButtonElement { + constructor () { + super() + + this._onClick = this._onClick.bind(this) + } + + connectedCallback () { + this.addEventListener('click', this._onClick) + } + + disconnectedCallback () { + this.removeEventListener('click', this._onClick) + } + + _onClick () { + this.dispatchEvent(new CustomEvent(EventName.nextPage, { bubbles: true })) + } + + /** + * + * @param {number} state + */ + setState (state) { + let text = this.idleText + let disabled = false + + if (state === State.loading) { + text = this.loadingText + disabled = true + } + + if (state === State.complete) { + this.style.display = 'none' + } + + this.innerText = text + this.disabled = disabled + } + + /** + * + * @returns {string} + */ + get loadingText () { + return this.getAttribute('loading-text') + } + + /** + * + * @returns {string} + */ + get idleText () { + return this.getAttribute('idle-text') + } +} diff --git a/Application/js/elements/post-attachment.js b/Application/js/elements/post-attachment.js new file mode 100644 index 0000000..c41b0f3 --- /dev/null +++ b/Application/js/elements/post-attachment.js @@ -0,0 +1,89 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +import { FileUpload } from './file-upload' +import { EventName } from '../dom/custom-events' + +export class PostAttachment extends FileUpload { + /** + * + * @private + */ + _render () { + this.classList.add('post-attachment') + + const $svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + $svg.classList.add('icon') + this.appendChild($svg) + + const $use = document.createElementNS('http://www.w3.org/2000/svg', 'use') + $use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/icons/attachment.svg#icon') + $svg.appendChild($use) + + const $name = document.createElement('span') + $name.classList.add('name') + $name.innerText = this._file.name + this.appendChild($name) + } + + /** + * + * @private + */ + _renderInput () { + const $input = document.createElement('input') + + $input.type = 'text' + $input.hidden = true + $input.required = true + $input.name = `attachments[]` + + this._$input = $input + + this.appendChild($input) + + setTimeout(() => { + this.dispatchEvent(new CustomEvent(EventName.formValidityChange, { bubbles: true })) + }) + } + + _renderUpload () { + const $progress = document.createElement('div') + $progress.classList.add('progress') + this.appendChild($progress) + + const $bar = document.createElement('div') + $bar.classList.add('bar') + $progress.appendChild($bar) + + const $svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + $svg.classList.add('done') + $progress.appendChild($svg) + + const $use = document.createElementNS('http://www.w3.org/2000/svg', 'use') + $use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/icons/done.svg#icon') + $svg.appendChild($use) + + this._$bar = $bar + } + + /** + * + * @param {{loaded: number, total: number}} progress + * @private + */ + _onProgress (progress) { + this._$bar.style.backgroundSize = `${100 / progress.total * progress.loaded}% 100%` + } + + /** + * + * @param {{}} data + * @private + */ + _onDone (data) { + this._$input.value = data['public_id'] + this.dispatchEvent(new CustomEvent(EventName.formValidityChange, { bubbles: true })) + } +} diff --git a/Application/js/elements/time-elements.js b/Application/js/elements/time-elements.js new file mode 100644 index 0000000..603376d --- /dev/null +++ b/Application/js/elements/time-elements.js @@ -0,0 +1,597 @@ +// Shout out to https://github.com/basecamp/local_time/blob/master/app/assets/javascripts/local_time.js.coffee +var weekdays = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; +var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; + +function pad (num) { + return ('0' + num).slice(-2); +} + +function makeFormatter (options) { + if ('Intl' in window) { + try { + return new window.Intl.DateTimeFormat(undefined, options); + } catch (e) { + if (!(e instanceof RangeError)) { + throw e; + } + } + } +} + +function strftime (time, formatString) { + var day = time.getDay(); + var date = time.getDate(); + var month = time.getMonth(); + var year = time.getFullYear(); + var hour = time.getHours(); + var minute = time.getMinutes(); + var second = time.getSeconds(); + return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, function (_arg) { + var match; + var modifier = _arg[ 1 ]; + switch (modifier) { + case '%': + return '%'; + case 'a': + return weekdays[ day ].slice(0, 3); + case 'A': + return weekdays[ day ]; + case 'b': + return months[ month ].slice(0, 3); + case 'B': + return months[ month ]; + case 'c': + return time.toString(); + case 'd': + return pad(date); + case 'e': + return date; + case 'H': + return pad(hour); + case 'I': + return pad(strftime(time, '%l')); + case 'l': + if (hour === 0 || hour === 12) { + return 12; + } else { + return (hour + 12) % 12; + } + break; + case 'm': + return pad(month + 1); + case 'M': + return pad(minute); + case 'p': + if (hour > 11) { + return 'PM'; + } else { + return 'AM'; + } + break; + case 'P': + if (hour > 11) { + return 'pm'; + } else { + return 'am'; + } + break; + case 'S': + return pad(second); + case 'w': + return day; + case 'y': + return pad(year % 100); + case 'Y': + return year; + case 'Z': + match = time.toString().match(/\((\w+)\)$/); + return match ? match[ 1 ] : ''; + case 'z': + match = time.toString().match(/\w([+-]\d\d\d\d) /); + return match ? match[ 1 ] : ''; + } + }); +} + +function RelativeTime (date) { + this.date = date; +} + +RelativeTime.prototype.toString = function () { + var ago = this.timeElapsed(); + if (ago) { + return ago; + } else { + var ahead = this.timeAhead(); + if (ahead) { + return ahead; + } else { + return 'on ' + this.formatDate(); + } + } +}; + +RelativeTime.prototype.timeElapsed = function () { + var ms = new Date().getTime() - this.date.getTime(); + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + if (ms >= 0 && day < 30) { + return this.timeAgoFromMs(ms); + } + else { + return null; + } +}; + +RelativeTime.prototype.timeAhead = function () { + var ms = this.date.getTime() - (new Date().getTime()); + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + if (ms >= 0 && day < 30) { + return this.timeUntil(); + } + else { + return null; + } +}; + +RelativeTime.prototype.timeAgo = function () { + var ms = new Date().getTime() - this.date.getTime(); + return this.timeAgoFromMs(ms); +}; + +RelativeTime.prototype.timeAgoFromMs = function (ms) { + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + var month = Math.round(day / 30); + var year = Math.round(month / 12); + if (ms < 0) { + return 'just now'; + } else if (sec < 10) { + return 'just now'; + } else if (sec < 45) { + return sec + ' seconds ago'; + } else if (sec < 90) { + return 'a minute ago'; + } else if (min < 45) { + return min + ' minutes ago'; + } else if (min < 90) { + return 'an hour ago'; + } else if (hr < 24) { + return hr + ' hours ago'; + } else if (hr < 36) { + return 'a day ago'; + } else if (day < 30) { + return day + ' days ago'; + } else if (day < 45) { + return 'a month ago'; + } else if (month < 12) { + return month + ' months ago'; + } else if (month < 18) { + return 'a year ago'; + } else { + return year + ' years ago'; + } +}; + +RelativeTime.prototype.microTimeAgo = function () { + var ms = new Date().getTime() - this.date.getTime(); + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + var month = Math.round(day / 30); + var year = Math.round(month / 12); + if (min < 1) { + return '1m'; + } else if (min < 60) { + return min + 'm'; + } else if (hr < 24) { + return hr + 'h'; + } else if (day < 365) { + return day + 'd'; + } else { + return year + 'y'; + } +}; + +RelativeTime.prototype.timeUntil = function () { + var ms = this.date.getTime() - (new Date().getTime()); + return this.timeUntilFromMs(ms); +}; + +RelativeTime.prototype.timeUntilFromMs = function (ms) { + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + var month = Math.round(day / 30); + var year = Math.round(month / 12); + if (month >= 18) { + return year + ' years from now'; + } else if (month >= 12) { + return 'a year from now'; + } else if (day >= 45) { + return month + ' months from now'; + } else if (day >= 30) { + return 'a month from now'; + } else if (hr >= 36) { + return day + ' days from now'; + } else if (hr >= 24) { + return 'a day from now'; + } else if (min >= 90) { + return hr + ' hours from now'; + } else if (min >= 45) { + return 'an hour from now'; + } else if (sec >= 90) { + return min + ' minutes from now'; + } else if (sec >= 45) { + return 'a minute from now'; + } else if (sec >= 10) { + return sec + ' seconds from now'; + } else { + return 'just now'; + } +}; + +RelativeTime.prototype.microTimeUntil = function () { + var ms = this.date.getTime() - (new Date().getTime()); + var sec = Math.round(ms / 1000); + var min = Math.round(sec / 60); + var hr = Math.round(min / 60); + var day = Math.round(hr / 24); + var month = Math.round(day / 30); + var year = Math.round(month / 12); + if (day >= 365) { + return year + 'y'; + } else if (hr >= 24) { + return day + 'd'; + } else if (min >= 60) { + return hr + 'h'; + } else if (min > 1) { + return min + 'm'; + } else { + return '1m'; + } +}; + +// Private: Determine if the day should be formatted before the month name in +// the user's current locale. For example, `9 Jun` for en-GB and `Jun 9` +// for en-US. +// +// Returns true if the day appears before the month. +function isDayFirst () { + if (dayFirst !== null) { + return dayFirst; + } + + var formatter = makeFormatter({ day: 'numeric', month: 'short' }); + if (formatter) { + var output = formatter.format(new Date(0)); + dayFirst = !!output.match(/^\d/); + return dayFirst; + } else { + return false; + } +} +var dayFirst = null; + +// Private: Determine if the year should be separated from the month and day +// with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US. +// +// Returns true if the date needs a separator. +function isYearSeparator () { + if (yearSeparator !== null) { + return yearSeparator; + } + + var formatter = makeFormatter({ day: 'numeric', month: 'short', year: 'numeric' }); + if (formatter) { + var output = formatter.format(new Date(0)); + yearSeparator = !!output.match(/\d,/); + return yearSeparator; + } else { + return true; + } +} +var yearSeparator = null; + +// Private: Determine if the date occurs in the same year as today's date. +// +// date - The Date to test. +// +// Returns true if it's this year. +function isThisYear (date) { + var now = new Date(); + return now.getUTCFullYear() === date.getUTCFullYear(); +} + +RelativeTime.prototype.formatDate = function () { + var format = isDayFirst() ? '%e %b' : '%b %e'; + if (!isThisYear(this.date)) { + format += isYearSeparator() ? ', %Y' : ' %Y'; + } + return strftime(this.date, format); +}; + +RelativeTime.prototype.formatTime = function () { + var formatter = makeFormatter({ hour: 'numeric', minute: '2-digit' }); + if (formatter) { + return formatter.format(this.date); + } else { + return strftime(this.date, '%l:%M%P'); + } +}; + +// Internal: Array tracking all elements attached to the document that need +// to be updated every minute. +var nowElements = []; + +// Internal: Timer ID for `updateNowElements` interval. +var updateNowElementsId; + +// Internal: Install a timer to refresh all attached relative-time elements every +// minute. +function updateNowElements () { + var time, i, len; + for (i = 0, len = nowElements.length; i < len; i++) { + time = nowElements[ i ]; + time.textContent = time.getFormattedDate(); + } +} + +class ExtendedTime extends HTMLElement { + + static observedAttributes () { + return [ 'datetime' ] + } + + attributeChangedCallback (attrName, oldValue, newValue) { + if (attrName === 'datetime') { + var millis = Date.parse(newValue); + this._date = isNaN(millis) ? null : new Date(millis); + } + + var title = this.getFormattedTitle(); + if (title) { + this.setAttribute('title', title); + } + + var text = this.getFormattedDate(); + if (text) { + this.textContent = text; + } + } + + getFormattedTitle () { + if (!this._date) { + return; + } + + if (this.hasAttribute('title')) { + return this.getAttribute('title'); + } + + var formatter = makeFormatter({ + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short' + }); + if (formatter) { + return formatter.format(this._date); + } else { + try { + return this._date.toLocaleString(); + } catch (e) { + if (e instanceof RangeError) { + return this._date.toString(); + } else { + throw e; + } + } + } + } + +} + +export class RelativeTimeElement extends ExtendedTime { + + constructor () { + super() + + var value = this.getAttribute('datetime'); + if (value) { + this.attributeChangedCallback('datetime', null, value); + } + } + + getFormattedDate () { + if (this._date) { + return new RelativeTime(this._date).toString(); + } + } + + connectedCallback () { + nowElements.push(this); + + if (!updateNowElementsId) { + updateNowElements(); + updateNowElementsId = setInterval(updateNowElements, 60 * 1000); + } + } + + disconnectedCallback () { + var ix = nowElements.indexOf(this); + if (ix !== -1) { + nowElements.splice(ix, 1); + } + + if (!nowElements.length) { + if (updateNowElementsId) { + clearInterval(updateNowElementsId); + updateNowElementsId = null; + } + } + } + +} + +export class TimeAgoElement extends RelativeTimeElement { + getFormattedDate () { + if (this._date) { + var format = this.getAttribute('format'); + if (format === 'micro') { + return new RelativeTime(this._date).microTimeAgo(); + } else { + return new RelativeTime(this._date).timeAgo(); + } + } + } +} + +export class TimeUntilElement extends RelativeTimeElement { + getFormattedDate () { + if (this._date) { + var format = this.getAttribute('format'); + if (format === 'micro') { + return new RelativeTime(this._date).microTimeUntil(); + } else { + return new RelativeTime(this._date).timeUntil(); + } + } + } +} + +export class LocalTimeElement extends ExtendedTime { + constructor () { + super() + + var value; + if (value = this.getAttribute('datetime')) { + this.attributeChangedCallback('datetime', null, value); + } + if (value = this.getAttribute('format')) { + this.attributeChangedCallback('format', null, value); + } + } + + getFormattedDate () { + if (!this._date) { + return; + } + + var date = formatDate(this) || ''; + var time = formatTime(this) || ''; + return (date + ' ' + time).trim(); + } + +} + +// Private: Format a date according to the `weekday`, `day`, `month`, +// and `year` attribute values. +// +// This doesn't use Intl.DateTimeFormat to avoid creating text in the user's +// language when the majority of the surrounding text is in English. There's +// currently no way to separate the language from the format in Intl. +// +// el - The local-time element to format. +// +// Returns a date String or null if no date formats are provided. +function formatDate (el) { + // map attribute values to strftime + var props = { + weekday: { + 'short': '%a', + 'long': '%A' + }, + day: { + 'numeric': '%e', + '2-digit': '%d' + }, + month: { + 'short': '%b', + 'long': '%B' + }, + year: { + 'numeric': '%Y', + '2-digit': '%y' + } + }; + + // build a strftime format string + var format = isDayFirst() ? 'weekday day month year' : 'weekday month day, year'; + for (var prop in props) { + var value = props[ prop ][ el.getAttribute(prop) ]; + format = format.replace(prop, value || ''); + } + + // clean up year separator comma + format = format.replace(/(\s,)|(,\s$)/, ''); + + // squeeze spaces from final string + return strftime(el._date, format).replace(/\s+/, ' ').trim(); +} + +// Private: Format a time according to the `hour`, `minute`, and `second` +// attribute values. +// +// el - The local-time element to format. +// +// Returns a time String or null if no time formats are provided. +function formatTime (el) { + // retrieve format settings from attributes + var options = { + hour: el.getAttribute('hour'), + minute: el.getAttribute('minute'), + second: el.getAttribute('second') + }; + + // remove unset format attributes + for (var opt in options) { + if (!options[ opt ]) { + delete options[ opt ]; + } + } + + // no time format attributes provided + if (Object.keys(options).length === 0) { + return; + } + + var formatter = makeFormatter(options); + if (formatter) { + // locale-aware formatting of 24 or 12 hour times + return formatter.format(el._date); + } else { + // fall back to strftime for non-Intl browsers + var timef = options.second ? '%H:%M:%S' : '%H:%M'; + return strftime(el._date, timef); + } +} + +// Public: RelativeTimeElement constructor. +// +// var time = new RelativeTimeElement() +// # => +// +// window.customElements.define('relative-time', RelativeTimeElement) + +// window.customElements.define('time-ago', TimeAgoElement) +// window.customElements.define('time-until', TimeUntilElement) + +// Public: LocalTimeElement constructor. +// +// var time = new LocalTimeElement() +// # => +// +// window.customElements.define('local-time', LocalTimeElement); + diff --git a/Application/js/elements/toast-message.js b/Application/js/elements/toast-message.js new file mode 100644 index 0000000..fa7b741 --- /dev/null +++ b/Application/js/elements/toast-message.js @@ -0,0 +1,103 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * @type {Element} + */ +let _toastContainer + +export function getToastContainer () { + if (_toastContainer) { + return _toastContainer + } + + _toastContainer = document.createElement('section') + _toastContainer.className = 'toast-container' + + document.body.appendChild(_toastContainer) + + return _toastContainer +} + +export class ToastMessage extends HTMLElement { + + constructor () { + super() + + this.classList.add('toast-message') + + this._onTtlExpired = this._onTtlExpired.bind(this) + } + + connectedCallback () { + if (this.toastTtl) { + this._timeout = setTimeout(this._onTtlExpired, this.toastTtl) + } + } + + disconnectedCallback () { + if (this._timeout) { + clearTimeout(this._timeout) + this._timeout = null + } + } + + /** + * + * @returns {Promise} + * @private + */ + _onTtlExpired () { + return this.hide() + } + + /** + * + * @returns {Promise} + */ + hide () { + return new Promise((resolve) => { + this.addEventListener( + 'animationend', + () => { + this.remove() + resolve() + }, + { once: true } + ) + + this.classList.add('-disappear') + }) + } + + /** + * + * @returns {Promise} + */ + show () { + const container = getToastContainer() + const toast = container.querySelector('toast-message') + const onHide = toast ? toast.hide() : Promise.resolve() + + return onHide.then(() => { + container.appendChild(this) + }) + } + + /** + * + * @returns {number} + */ + get toastTtl () { + return Number.parseInt(this.getAttribute('toast-ttl')) || null; + } + + /** + * + * @param {number} ttl + */ + set toastTtl (ttl) { + this.setAttribute('toast-ttl', ttl.toString()) + } +} diff --git a/Application/js/elements/validated-input.js b/Application/js/elements/validated-input.js new file mode 100644 index 0000000..621fbb6 --- /dev/null +++ b/Application/js/elements/validated-input.js @@ -0,0 +1,61 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +import { defer } from '../dom/next-render.js' +import { getByteSize } from '../dom/string.js' + +export class ValidatedInput extends HTMLInputElement { + constructor () { + super() + + this._validate = this._validate.bind(this) + + defer(() => this._validate()) + } + + connectedCallback () { + this.addEventListener('input', this._validate) + } + + disconnectedCallback () { + this.removeEventListener('input', this._validate) + } + + _validate () { + this.setCustomValidity(this._getValidity()) + } + + /** + * + * @returns {string} + * @private + */ + _getValidity () { + if (this.maxByteSize) { + return this._validateMaxByteSize() + } + + return '' + } + + /** + * + * @returns {string} + * @private + */ + _validateMaxByteSize () { + if (getByteSize(this.value) > this.maxByteSize) { + return `max size of ${this.maxByteSize} exceeded` + } + + return '' + } + + /** + * + * @returns {number|null} + */ + get maxByteSize () { + return Number.parseInt(this.getAttribute('max-byte-size')) || null; + } +} diff --git a/Application/js/event/signal.js b/Application/js/event/signal.js new file mode 100644 index 0000000..aeecd8a --- /dev/null +++ b/Application/js/event/signal.js @@ -0,0 +1,43 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +/** + * + * @template T + * @constructor + */ +export function Signal () { +} + +/** + * + * @param {(function(value: T))} callbackFn + */ +Signal.prototype.addListener = function (callbackFn) { + if (this._listeners == null) { + this._listeners = new Set() + } + + this._listeners.add(callbackFn) +} + +Signal.prototype.removeListener = function (callbackFn) { + if (this._listeners == null) { + return + } + + this._listeners.delete(callbackFn) +} + +/** + * + * @param {T} [value] + */ +Signal.prototype.dispatch = function (value) { + if (this._listeners == null) { + return + } + + this._listeners.forEach((listener) => listener(value)) +} diff --git a/Application/polyfills/README.md b/Application/polyfills/README.md new file mode 100644 index 0000000..87a4124 --- /dev/null +++ b/Application/polyfills/README.md @@ -0,0 +1,12 @@ +#### [custom-elements.js](custom-elements.js) + +Custom elements polyfill by [Ruben Schmidmeister](https://github.com/bash/custom-elements), licensed under the [WTFPL](https://raw.githubusercontent.com/bash/custom-elements/master/LICENSE). + +#### [fetch.js](fetch.js) + +Fetch polyfill by [GitHub](https://github.com/github/fetch), licensed under the [MIT license](https://github.com/github/fetch/blob/master/LICENSE). + + +#### [dom4.js](dom4.js) + +Source: [WebReflection/dom4](https://github.com/WebReflection/dom4), licensed under the [MIT license](https://github.com/WebReflection/dom4/blob/master/LICENSE.txt) diff --git a/Application/polyfills/custom-elements.js b/Application/polyfills/custom-elements.js new file mode 100644 index 0000000..900d268 --- /dev/null +++ b/Application/polyfills/custom-elements.js @@ -0,0 +1 @@ +!function(){"use strict";function e(e){Promise.resolve().then(e)}function t(e){return null==e.parentNode?e:t(e.parentNode)}function n(e){return t(e).nodeType===window.Node.DOCUMENT_NODE}function o(e){return"custom"===window.customElements._getState(e)}function r(e){return function t(){var n=Object.getPrototypeOf(this);if(n===t.prototype)throw new TypeError("Illegal Constructor");var o=e._lookupByConstructor(this.constructor);if(null==o)throw new Error("no definition found for element");var r=o.constructionStack,i=o.prototype;if(!r.length){var a=document.createElement(o.localName);return o.localName!==o.name&&a.setAttribute("is",o.name),Reflect.setPrototypeOf(a,i),e._customElementState.set(a,"custom"),a}var c=r[r.length-1];if(c===w)throw new Error("invalid state e.g. nested element construction before calling super()");return Reflect.setPrototypeOf(c,i),r[r.length-1]=w,c}}function i(e){var t=window.HTMLElement,n=r(e);n.prototype=Object.create(t.prototype),window.HTMLElement=n,b.forEach(function(e){var t="HTML"+e+"Element",n=window[t];if(n){var o=n.prototype;Object.setPrototypeOf(o,window.HTMLElement.prototype);var r=function(){return window.HTMLElement.apply(this,arguments)};r.prototype=o,window[t]=r}})}function a(){var e="dummy-button-"+Date.now();try{var t=function(e){function t(){return l(this,t),d(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return f(t,e),t}(window.HTMLButtonElement);window.customElements.define(e,t,{extends:"button"});var n=document.createElement("button",{is:e});if(!(n instanceof t))return!1;if(n.getAttribute("is")!==e)return!1}catch(e){return!1}return!0}function c(e,t){return function(n){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=e.call(this,n),i=o.is,a=t._getElementDefinition(r,i);if(null!=i&&null==a)throw new Error("no definition found for element "+n);return null!=a&&t._upgradeElement(r,a),null!=i&&r.setAttribute("is",o.is),r}}var u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},l=(function(){function e(e){this.value=e}function t(t){function n(e,t){return new Promise(function(n,r){var c={key:e,arg:t,resolve:n,reject:r,next:null};a?a=a.next=c:(i=a=c,o(e,t))})}function o(n,i){try{var a=t[n](i),c=a.value;c instanceof e?Promise.resolve(c.value).then(function(e){o("next",e)},function(e){o("throw",e)}):r(a.done?"return":"normal",a.value)}catch(e){r("throw",e)}}function r(e,t){switch(e){case"return":i.resolve({value:t,done:!0});break;case"throw":i.reject(t);break;default:i.resolve({value:t,done:!1})}i=i.next,i?o(i.key,i.arg):a=null}var i,a;this._invoke=n,"function"!=typeof t.return&&(this.return=void 0)}return"function"==typeof Symbol&&Symbol.asyncIterator&&(t.prototype[Symbol.asyncIterator]=function(){return this}),t.prototype.next=function(e){return this._invoke("next",e)},t.prototype.throw=function(e){return this._invoke("throw",e)},t.prototype.return=function(e){return this._invoke("return",e)},{wrap:function(e){return function(){return new t(e.apply(this,arguments))}},await:function(t){return new e(t)}}}(),function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}),s=function(){function e(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:{};if(e=e.toLowerCase(),"function"!=typeof t)throw new TypeError("constructor is not a constructor");if(!p(e))throw new SyntaxError("the element name "+e+" is not valid");if(this._names.has(e))throw new Error("an element with name '"+e+"' is already defined");if(this._constructors.has(t))throw new Error("this constructor is already registered");var r=e,i=o.extends||null;if(null!==i){if(p(i))throw new Error("extends must be a native element");r=i}var a=t.prototype;if("object"!==("undefined"==typeof a?"undefined":u(a)))throw new TypeError("constructor.prototype must be an object");var c={connectedCallback:null,disconnectedCallback:null,adoptedCallback:null,attributeChangedCallback:null};Object.keys(c).forEach(function(e){var t=a[e];void 0!==t&&(c[e]=t)});var l=[];if(null!==c.attributeChangedCallback){var s=t.observedAttributes;null!=s&&(l=Array.from(s).map(function(e){return String(e)}))}var f={name:e,localName:r,constructor:t,prototype:a,observedAttributes:l,lifecycleCallbacks:c,constructionStack:[]};this._definitions[e]=f,this._names.add(e),this._constructors.add(t);var d=window.document,h=r;null!=i&&(h+='[is="'+e+'"]');var m=d.querySelectorAll(h);if(Array.from(m).forEach(function(e){return n._upgradeElement(e,f)}),this._whenDefined.hasOwnProperty(e)){var y=this._whenDefined[e];y.resolve(),delete this._whenDefined[e]}}},{key:"_upgradeElement",value:function(e,t){var o=this._getState(e);if("custom"!==o&&"failed"!==o){for(var r=0;r3?a(o):null,y=String(o.key),b=String(o.char),w=o.location,E=o.keyCode||(o.keyCode=y)&&y.charCodeAt(0)||0,S=o.charCode||(o.charCode=b)&&b.charCodeAt(0)||0,x=o.bubbles,T=o.cancelable,N=o.repeat,C=o.locale,k=o.view||e,L;o.which||(o.which=o.keyCode);if("initKeyEvent"in u)u.initKeyEvent(t,x,T,k,h,d,p,v,E,S);else if(0 -1) ? upcased : method + } + + function Request(input, options) { + options = options || {} + var body = options.body + if (Request.prototype.isPrototypeOf(input)) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url + this.credentials = input.credentials + if (!options.headers) { + this.headers = new Headers(input.headers) + } + this.method = input.method + this.mode = input.mode + if (!body) { + body = input._bodyInit + input.bodyUsed = true + } + } else { + this.url = input + } + + this.credentials = options.credentials || this.credentials || 'omit' + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers) + } + this.method = normalizeMethod(options.method || this.method || 'GET') + this.mode = options.mode || this.mode || null + this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body) + } + + Request.prototype.clone = function() { + return new Request(this) + } + + function decode(body) { + var form = new FormData() + body.trim().split('&').forEach(function(bytes) { + if (bytes) { + var split = bytes.split('=') + var name = split.shift().replace(/\+/g, ' ') + var value = split.join('=').replace(/\+/g, ' ') + form.append(decodeURIComponent(name), decodeURIComponent(value)) + } + }) + return form + } + + function headers(xhr) { + var head = new Headers() + var pairs = (xhr.getAllResponseHeaders() || '').trim().split('\n') + pairs.forEach(function(header) { + var split = header.trim().split(':') + var key = split.shift().trim() + var value = split.join(':').trim() + head.append(key, value) + }) + return head + } + + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this.type = 'default' + this.status = options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText + this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) + this.url = options.url || '' + this._initBody(bodyInit) + } + + Body.call(Response.prototype) + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + } + + Response.error = function() { + var response = new Response(null, {status: 0, statusText: ''}) + response.type = 'error' + return response + } + + var redirectStatuses = [301, 302, 303, 307, 308] + + Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) + } + + self.Headers = Headers + self.Request = Request + self.Response = Response + + self.fetch = function(input, init) { + return new Promise(function(resolve, reject) { + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) + } + + var xhr = new XMLHttpRequest() + + function responseURL() { + if ('responseURL' in xhr) { + return xhr.responseURL + } + + // Avoid security warnings on getResponseHeader when not allowed by CORS + if (/^X-Request-URL:/mi.test(xhr.getAllResponseHeaders())) { + return xhr.getResponseHeader('X-Request-URL') + } + + return + } + + xhr.onload = function() { + var options = { + status: xhr.status, + statusText: xhr.statusText, + headers: headers(xhr), + url: responseURL() + } + var body = 'response' in xhr ? xhr.response : xhr.responseText + resolve(new Response(body, options)) + } + + xhr.onerror = function() { + reject(new TypeError('Network request failed')) + } + + xhr.ontimeout = function() { + reject(new TypeError('Network request failed')) + } + + xhr.open(request.method, request.url, true) + + if (request.credentials === 'include') { + xhr.withCredentials = true + } + + if ('responseType' in xhr && support.blob) { + xhr.responseType = 'blob' + } + + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value) + }) + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) + }) + } + self.fetch.polyfill = true +})(typeof self !== 'undefined' ? self : this); diff --git a/Application/polyfills/object.js b/Application/polyfills/object.js new file mode 100644 index 0000000..d70431f --- /dev/null +++ b/Application/polyfills/object.js @@ -0,0 +1,17 @@ +/** + * (c) 2016 timetab.io + */ + +if (!('entries' in Object)) { + /** + * + * @param {{}} object + * @returns {Array>} + */ + Object.entries = function (object) { + return Object.keys(object) + .map(function (key) { + return [ key, object[ key ] ] + }) + } +} diff --git a/Framework/Rakefile b/Framework/Rakefile new file mode 100644 index 0000000..63cd908 --- /dev/null +++ b/Framework/Rakefile @@ -0,0 +1,20 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src'), + gen_autoload('lib'), + gen_autoload('tests') +] + +multitask default: TARGETS + +desc 'Run tests' +task :test do + sh 'phpunit' +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Framework/bootstrap.php b/Framework/bootstrap.php new file mode 100644 index 0000000..9a12a87 --- /dev/null +++ b/Framework/bootstrap.php @@ -0,0 +1,5 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Client +{ + /** + * @var Transport + */ + public $transport; + + /** + * @var array + */ + protected $params; + + /** + * @var IndicesNamespace + */ + protected $indices; + + /** + * @var ClusterNamespace + */ + protected $cluster; + + /** + * @var NodesNamespace + */ + protected $nodes; + + /** + * @var SnapshotNamespace + */ + protected $snapshot; + + /** + * @var CatNamespace + */ + protected $cat; + + /** + * @var IngestNamespace + */ + protected $ingest; + + /** + * @var TasksNamespace + */ + protected $tasks; + + /** @var callback */ + protected $endpoints; + + /** @var NamespaceBuilderInterface[] */ + protected $registeredNamespaces = []; + + /** + * Client constructor + * + * @param Transport $transport + * @param callable $endpoint + * @param AbstractNamespace[] $registeredNamespaces + */ + public function __construct(Transport $transport, callable $endpoint, array $registeredNamespaces) + { + $this->transport = $transport; + $this->endpoints = $endpoint; + $this->indices = new IndicesNamespace($transport, $endpoint); + $this->cluster = new ClusterNamespace($transport, $endpoint); + $this->nodes = new NodesNamespace($transport, $endpoint); + $this->snapshot = new SnapshotNamespace($transport, $endpoint); + $this->cat = new CatNamespace($transport, $endpoint); + $this->ingest = new IngestNamespace($transport, $endpoint); + $this->tasks = new TasksNamespace($transport, $endpoint); + $this->registeredNamespaces = $registeredNamespaces; + } + + /** + * @param $params + * @return array + */ + public function info($params = []) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Info $endpoint */ + $endpoint = $endpointBuilder('Info'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * @param $params array Associative array of parameters + * + * @return bool + */ + public function ping($params = []) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Ping $endpoint */ + $endpoint = $endpointBuilder('Ping'); + $endpoint->setParams($params); + + try { + $this->performRequest($endpoint); + } catch (Missing404Exception $exception) { + return false; + } catch (TransportException $exception) { + return false; + } + + return true; + } + + /** + * $params['id'] = (string) The document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (use `_all` to fetch the first document matching the ID across all types) (Required) + * ['ignore_missing'] = ?? + * ['fields'] = (list) A comma-separated list of fields to return in the response + * ['parent'] = (string) The ID of the parent document + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['realtime'] = (boolean) Specify whether to perform the operation in realtime or search mode + * ['refresh'] = (boolean) Refresh the shard containing the document before performing the operation + * ['routing'] = (string) Specific routing value + * ['_source'] = (list) True or false to return the _source field or not, or a list of fields to return + * ['_source_exclude'] = (list) A list of fields to exclude from the returned _source field + * ['_source_include'] = (list) A list of fields to extract and return from the _source field + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function get($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Get $endpoint */ + $endpoint = $endpointBuilder('Get'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (use `_all` to fetch the first document matching the ID across all types) (Required) + * ['ignore_missing'] = ?? + * ['parent'] = (string) The ID of the parent document + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['realtime'] = (boolean) Specify whether to perform the operation in realtime or search mode + * ['refresh'] = (boolean) Refresh the shard containing the document before performing the operation + * ['routing'] = (string) Specific routing value + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getSource($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Get $endpoint */ + $endpoint = $endpointBuilder('Get'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type) + ->returnOnlySource(); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (Required) + * ['consistency'] = (enum) Specific write consistency setting for the operation + * ['parent'] = (string) ID of parent document + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['replication'] = (enum) Specific replication type + * ['routing'] = (string) Specific routing value + * ['timeout'] = (time) Explicit operation timeout + * ['version_type'] = (enum) Specific version type + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function delete($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + + $this->verifyNotNullOrEmpty("id", $id); + $this->verifyNotNullOrEmpty("type", $type); + $this->verifyNotNullOrEmpty("index", $index); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Delete $endpoint */ + $endpoint = $endpointBuilder('Delete'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of indices to restrict the results + * ['type'] = (list) A comma-separated list of types to restrict the results + * ['min_score'] = (number) Include only documents with a specific `_score` value in the result + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['routing'] = (string) Specific routing value + * ['source'] = (string) The URL-encoded query definition (instead of using the request body) + * ['body'] = (array) A query to restrict the results (optional) + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function count($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Count $endpoint */ + $endpoint = $endpointBuilder('Count'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of indices to restrict the results + * ['type'] = (list) A comma-separated list of types to restrict the results + * ['id'] = (string) ID of document + * ['ignore_unavailable'] = (boolean) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['routing'] = (string) Specific routing value + * ['allow_no_indices'] = (boolean) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['body'] = (array) A query to restrict the results (optional) + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['percolate_index'] = (string) The index to count percolate the document into. Defaults to index. + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * ['version'] = (number) Explicit version number for concurrency control + * ['version_type'] = (enum) Specific version type + * + * @param $params array Associative array of parameters + * + * @return array + * + * @deprecated + */ + public function countPercolate($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $id = $this->extractArgument($params, 'id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\CountPercolate $endpoint */ + $endpoint = $endpointBuilder('CountPercolate'); + $endpoint->setIndex($index) + ->setType($type) + ->setID($id) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index with a registered percolator query (Required) + * ['type'] = (string) The document type (Required) + * ['prefer_local'] = (boolean) With `true`, specify that a local shard should be used if available, with `false`, use a random shard (default: true) + * ['body'] = (array) The document (`doc`) to percolate against registered queries; optionally also a `query` to limit the percolation to specific registered queries + * + * @param $params array Associative array of parameters + * + * @return array + * + * @deprecated + */ + public function percolate($params) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $id = $this->extractArgument($params, 'id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Percolate $endpoint */ + $endpoint = $endpointBuilder('Percolate'); + $endpoint->setIndex($index) + ->setType($type) + ->setID($id) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) Default index for items which don't provide one + * ['type'] = (string) Default document type for items which don't provide one + * ['ignore_unavailable'] = (boolean) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (boolean) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + * + * @deprecated + */ + public function mpercolate($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\MPercolate $endpoint */ + $endpoint = $endpointBuilder('MPercolate'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) Default index for items which don't provide one + * ['type'] = (string) Default document type for items which don't provide one + * ['term_statistics'] = (boolean) Specifies if total term frequency and document frequency should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['field_statistics'] = (boolean) Specifies if document count, sum of document frequencies and sum of total term frequencies should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['fields'] = (list) A comma-separated list of fields to return. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['offsets'] = (boolean) Specifies if term offsets should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['positions'] = (boolean) Specifies if term positions should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['payloads'] = (boolean) Specifies if term payloads should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) .Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['routing'] = (string) Specific routing value. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['parent'] = (string) Parent id of documents. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['realtime'] = (boolean) Specifies if request is real-time as opposed to near-real-time (default: true). + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function termvectors($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $id = $this->extractArgument($params, 'id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\TermVectors $endpoint */ + $endpoint = $endpointBuilder('TermVectors'); + $endpoint->setIndex($index) + ->setType($type) + ->setID($id) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) Default index for items which don't provide one + * ['type'] = (string) Default document type for items which don't provide one + * ['ids'] = (list) A comma-separated list of documents ids. You must define ids as parameter or set \"ids\" or \"docs\" in the request body + * ['term_statistics'] = (boolean) Specifies if total term frequency and document frequency should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['field_statistics'] = (boolean) Specifies if document count, sum of document frequencies and sum of total term frequencies should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['fields'] = (list) A comma-separated list of fields to return. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['offsets'] = (boolean) Specifies if term offsets should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['positions'] = (boolean) Specifies if term positions should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\"." + * ['payloads'] = (boolean) Specifies if term payloads should be returned. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) .Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['routing'] = (string) Specific routing value. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['parent'] = (string) Parent id of documents. Applies to all returned documents unless otherwise specified in body \"params\" or \"docs\". + * ['realtime'] = (boolean) Specifies if request is real-time as opposed to near-real-time (default: true). + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function mtermvectors($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\MTermVectors $endpoint */ + $endpoint = $endpointBuilder('MTermVectors'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (use `_all` to fetch the first document matching the ID across all types) (Required) + * ['parent'] = (string) The ID of the parent document + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['realtime'] = (boolean) Specify whether to perform the operation in realtime or search mode + * ['refresh'] = (boolean) Refresh the shard containing the document before performing the operation + * ['routing'] = (string) Specific routing value + * + * @param $params array Associative array of parameters + * + * @return array | boolean + */ + public function exists($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + + //manually make this verbose so we can check status code + $params['client']['verbose'] = true; + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Exists $endpoint */ + $endpoint = $endpointBuilder('Exists'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return BooleanRequestWrapper::performRequest($endpoint, $this->transport); + } + + /** + * $params['index'] = (string) The name of the index + * ['type'] = (string) The type of the document + * ['fields'] = (list) A comma-separated list of fields to return in the response + * ['parent'] = (string) The ID of the parent document + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['realtime'] = (boolean) Specify whether to perform the operation in realtime or search mode + * ['refresh'] = (boolean) Refresh the shard containing the document before performing the operation + * ['routing'] = (string) Specific routing value + * ['body'] = (array) Document identifiers; can be either `docs` (containing full document information) or `ids` (when index and type is provided in the URL. + * ['_source'] = (list) True or false to return the _source field or not, or a list of fields to return + * ['_source_exclude'] = (list) A list of fields to exclude from the returned _source field + * ['_source_include'] = (list) A list of fields to extract and return from the _source field + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function mget($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Mget $endpoint */ + $endpoint = $endpointBuilder('Mget'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to use as default + * ['type'] = (list) A comma-separated list of document types to use as default + * ['search_type'] = (enum) Search operation type + * ['body'] = (array|string) The request definitions (metadata-search request definition pairs), separated by newlines + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function msearch($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Msearch $endpoint */ + $endpoint = $endpointBuilder('Msearch'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (Required) + * ['id'] = (string) Specific document ID (when the POST method is used) + * ['consistency'] = (enum) Explicit write consistency setting for the operation + * ['parent'] = (string) ID of the parent document + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['replication'] = (enum) Specific replication type + * ['routing'] = (string) Specific routing value + * ['timeout'] = (time) Explicit operation timeout + * ['timestamp'] = (time) Explicit timestamp for the document + * ['ttl'] = (duration) Expiration time for the document + * ['version'] = (number) Explicit version number for concurrency control + * ['version_type'] = (enum) Specific version type + * ['body'] = (array) The document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function create($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Create $endpoint */ + $endpoint = $endpointBuilder('Create'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) Default index for items which don't provide one + * ['type'] = (string) Default document type for items which don't provide one + * ['consistency'] = (enum) Explicit write consistency setting for the operation + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['replication'] = (enum) Explicitly set the replication type + * ['fields'] = (list) Default comma-separated list of fields to return in the response for updates + * ['body'] = (array) The document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function bulk($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Bulk $endpoint */ + $endpoint = $endpointBuilder('Bulk'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (Required) + * ['id'] = (string) Specific document ID (when the POST method is used) + * ['consistency'] = (enum) Explicit write consistency setting for the operation + * ['op_type'] = (enum) Explicit operation type + * ['parent'] = (string) ID of the parent document + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['replication'] = (enum) Specific replication type + * ['routing'] = (string) Specific routing value + * ['timeout'] = (time) Explicit operation timeout + * ['timestamp'] = (time) Explicit timestamp for the document + * ['ttl'] = (duration) Expiration time for the document + * ['version'] = (number) Explicit version number for concurrency control + * ['version_type'] = (enum) Specific version type + * ['body'] = (array) The document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function index($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Index $endpoint */ + $endpoint = $endpointBuilder('Index'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['refresh'] = (boolean) Should the effected indexes be refreshed? + * ['timeout'] = (time) Time each individual bulk request should wait for shards that are unavailable + * ['consistency'] = (enum) Explicit write consistency setting for the operation + * ['wait_for_completion'] = (boolean) Should the request should block until the reindex is complete + * ['requests_per_second'] = (float) The throttle for this request in sub-requests per second. 0 means set no throttle + * ['body'] = (array) The search definition using the Query DSL and the prototype for the index request (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function reindex($params) + { + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + /** @var \Elasticsearch\Endpoints\Reindex $endpoint */ + $endpoint = $endpointBuilder('Reindex'); + $endpoint->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to restrict the operation; use `_all` or empty string to perform the operation on all indices + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['routing'] = (string) Specific routing value + * ['source'] = (string) The URL-encoded request definition (instead of using request body) + * ['body'] = (array) The request definition + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function suggest($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Suggest $endpoint */ + $endpoint = $endpointBuilder('Suggest'); + $endpoint->setIndex($index) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (Required) + * ['analyze_wildcard'] = (boolean) Specify whether wildcards and prefix queries in the query string query should be analyzed (default: false) + * ['analyzer'] = (string) The analyzer for the query string query + * ['default_operator'] = (enum) The default operator for query string query (AND or OR) + * ['df'] = (string) The default field for query string query (default: _all) + * ['fields'] = (list) A comma-separated list of fields to return in the response + * ['lenient'] = (boolean) Specify whether format-based query failures (such as providing text to a numeric field) should be ignored + * ['lowercase_expanded_terms'] = (boolean) Specify whether query terms should be lowercased + * ['parent'] = (string) The ID of the parent document + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['q'] = (string) Query in the Lucene query string syntax + * ['routing'] = (string) Specific routing value + * ['source'] = (string) The URL-encoded query definition (instead of using the request body) + * ['_source'] = (list) True or false to return the _source field or not, or a list of fields to return + * ['_source_exclude'] = (list) A list of fields to exclude from the returned _source field + * ['_source_include'] = (list) A list of fields to extract and return from the _source field + * ['body'] = (string) The URL-encoded query definition (instead of using the request body) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function explain($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Explain $endpoint */ + $endpoint = $endpointBuilder('Explain'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to search; use `_all` or empty string to perform the operation on all indices + * ['type'] = (list) A comma-separated list of document types to search; leave empty to perform the operation on all types + * ['analyzer'] = (string) The analyzer to use for the query string + * ['analyze_wildcard'] = (boolean) Specify whether wildcard and prefix queries should be analyzed (default: false) + * ['default_operator'] = (enum) The default operator for query string query (AND or OR) + * ['df'] = (string) The field to use as default where no field prefix is given in the query string + * ['explain'] = (boolean) Specify whether to return detailed information about score computation as part of a hit + * ['fields'] = (list) A comma-separated list of fields to return as part of a hit + * ['from'] = (number) Starting offset (default: 0) + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['indices_boost'] = (list) Comma-separated list of index boosts + * ['lenient'] = (boolean) Specify whether format-based query failures (such as providing text to a numeric field) should be ignored + * ['lowercase_expanded_terms'] = (boolean) Specify whether query terms should be lowercased + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['q'] = (string) Query in the Lucene query string syntax + * ['query_cache'] = (boolean) Enable query cache for this request + * ['request_cache'] = (boolean) Enable request cache for this request + * ['routing'] = (list) A comma-separated list of specific routing values + * ['scroll'] = (duration) Specify how long a consistent view of the index should be maintained for scrolled search + * ['search_type'] = (enum) Search operation type + * ['size'] = (number) Number of hits to return (default: 10) + * ['sort'] = (list) A comma-separated list of : pairs + * ['source'] = (string) The URL-encoded request definition using the Query DSL (instead of using request body) + * ['_source'] = (list) True or false to return the _source field or not, or a list of fields to return + * ['_source_exclude'] = (list) A list of fields to exclude from the returned _source field + * ['_source_include'] = (list) A list of fields to extract and return from the _source field + * ['stats'] = (list) Specific 'tag' of the request for logging and statistical purposes + * ['suggest_field'] = (string) Specify which field to use for suggestions + * ['suggest_mode'] = (enum) Specify suggest mode + * ['suggest_size'] = (number) How many suggestions to return in response + * ['suggest_text'] = (text) The source text for which the suggestions should be returned + * ['timeout'] = (time) Explicit operation timeout + * ['version'] = (boolean) Specify whether to return document version as part of a hit + * ['body'] = (array|string) The search definition using the Query DSL + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function search($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Search $endpoint */ + $endpoint = $endpointBuilder('Search'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to search; use `_all` or empty string to perform the operation on all indices + * ['type'] = (list) A comma-separated list of document types to search; leave empty to perform the operation on all types + * ['preference'] = (string) Specify the node or shard the operation should be performed on (default: random) + * ['routing'] = (string) Specific routing value + * ['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function searchShards($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\SearchShards $endpoint */ + $endpoint = $endpointBuilder('SearchShards'); + $endpoint->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to search; use `_all` or empty string to perform the operation on all indices + * ['type'] = (list) A comma-separated list of document types to search; leave empty to perform the operation on all types + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function searchTemplate($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Search $endpoint */ + $endpoint = $endpointBuilder('SearchTemplate'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['scroll_id'] = (string) The scroll ID for scrolled search + * ['scroll'] = (duration) Specify how long a consistent view of the index should be maintained for scrolled search + * ['body'] = (string) The scroll ID for scrolled search + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function scroll($params = array()) + { + $scrollID = $this->extractArgument($params, 'scroll_id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Scroll $endpoint */ + $endpoint = $endpointBuilder('Scroll'); + $endpoint->setScrollID($scrollID) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['scroll_id'] = (string) The scroll ID for scrolled search + * ['scroll'] = (duration) Specify how long a consistent view of the index should be maintained for scrolled search + * ['body'] = (string) The scroll ID for scrolled search + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function clearScroll($params = array()) + { + $scrollID = $this->extractArgument($params, 'scroll_id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Scroll $endpoint */ + $endpoint = $endpointBuilder('Scroll'); + $endpoint->setScrollID($scrollID) + ->setBody($body) + ->setClearScroll(true); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) Document ID (Required) + * ['index'] = (string) The name of the index (Required) + * ['type'] = (string) The type of the document (Required) + * ['consistency'] = (enum) Explicit write consistency setting for the operation + * ['fields'] = (list) A comma-separated list of fields to return in the response + * ['lang'] = (string) The script language (default: mvel) + * ['parent'] = (string) ID of the parent document + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['replication'] = (enum) Specific replication type + * ['retry_on_conflict'] = (number) Specify how many times should the operation be retried when a conflict occurs (default: 0) + * ['routing'] = (string) Specific routing value + * ['script'] = () The URL-encoded script definition (instead of using request body) + * ['timeout'] = (time) Explicit operation timeout + * ['timestamp'] = (time) Explicit timestamp for the document + * ['ttl'] = (duration) Expiration time for the document + * ['version_type'] = (number) Explicit version number for concurrency control + * ['body'] = (array) The request definition using either `script` or partial `doc` + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function update($params) + { + $id = $this->extractArgument($params, 'id'); + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Update $endpoint */ + $endpoint = $endpointBuilder('Update'); + $endpoint->setID($id) + ->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The script ID (Required) + * ['lang'] = (string) The script language (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getScript($params) + { + $id = $this->extractArgument($params, 'id'); + $lang = $this->extractArgument($params, 'lang'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Script\Get $endpoint */ + $endpoint = $endpointBuilder('Script\Get'); + $endpoint->setID($id) + ->setLang($lang); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The script ID (Required) + * ['lang'] = (string) The script language (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteScript($params) + { + $id = $this->extractArgument($params, 'id'); + $lang = $this->extractArgument($params, 'lang'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Script\Delete $endpoint */ + $endpoint = $endpointBuilder('Script\Delete'); + $endpoint->setID($id) + ->setLang($lang); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The script ID (Required) + * ['lang'] = (string) The script language (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putScript($params) + { + $id = $this->extractArgument($params, 'id'); + $lang = $this->extractArgument($params, 'lang'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Script\Put $endpoint */ + $endpoint = $endpointBuilder('Script\Put'); + $endpoint->setID($id) + ->setLang($lang) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The search template ID (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getTemplate($params) + { + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Template\Get $endpoint */ + $endpoint = $endpointBuilder('Template\Get'); + $endpoint->setID($id); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The search template ID (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteTemplate($params) + { + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Template\Delete $endpoint */ + $endpoint = $endpointBuilder('Template\Delete'); + $endpoint->setID($id); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) The search template ID (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putTemplate($params) + { + $id = $this->extractArgument($params, 'id'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Template\Put $endpoint */ + $endpoint = $endpointBuilder('Template\Put'); + $endpoint->setID($id) + ->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of indices to restrict the results + * ['fields'] = (list) A comma-separated list of fields for to get field statistics for (min value, max value, and more) + * ['level'] = (enum) Defines if field stats should be returned on a per index level or on a cluster wide level + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function fieldStats($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\FieldStats $endpoint */ + $endpoint = $endpointBuilder('FieldStats'); + $endpoint->setIndex($index) + ->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['id'] = (string) ID of the template to render + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function renderSearchTemplate($params = array()) + { + $body = $this->extractArgument($params, 'body'); + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\RenderSearchTemplate $endpoint */ + $endpoint = $endpointBuilder('RenderSearchTemplate'); + $endpoint->setBody($body) + ->setID($id); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * Operate on the Indices Namespace of commands + * + * @return IndicesNamespace + */ + public function indices() + { + return $this->indices; + } + + /** + * Operate on the Cluster namespace of commands + * + * @return ClusterNamespace + */ + public function cluster() + { + return $this->cluster; + } + + /** + * Operate on the Nodes namespace of commands + * + * @return NodesNamespace + */ + public function nodes() + { + return $this->nodes; + } + + /** + * Operate on the Snapshot namespace of commands + * + * @return SnapshotNamespace + */ + public function snapshot() + { + return $this->snapshot; + } + + /** + * Operate on the Cat namespace of commands + * + * @return CatNamespace + */ + public function cat() + { + return $this->cat; + } + + /** + * Operate on the Ingest namespace of commands + * + * @return IngestNamespace + */ + public function ingest() + { + return $this->ingest; + } + + /** + * Operate on the Tasks namespace of commands + * + * @return TasksNamespace + */ + public function tasks() + { + return $this->tasks; + } + + /** + * Catchall for registered namespaces + * + * @param $name + * @param $arguments + * @return Object + * @throws BadMethodCallException if the namespace cannot be found + */ + public function __call($name, $arguments) + { + if (isset($this->registeredNamespaces[$name])) { + return $this->registeredNamespaces[$name]; + } + throw new BadMethodCallException("Namespace [$name] not found"); + } + + /** + * @param array $params + * @param string $arg + * + * @return null|mixed + */ + public function extractArgument(&$params, $arg) + { + if (is_object($params) === true) { + $params = (array) $params; + } + + if (isset($params[$arg]) === true) { + $val = $params[$arg]; + unset($params[$arg]); + + return $val; + } else { + return null; + } + } + + private function verifyNotNullOrEmpty($name, $var) + { + if ($var === null) { + throw new InvalidArgumentException("$name cannot be null."); + } + + if (is_string($var)) { + if (strlen($var) === 0) { + throw new InvalidArgumentException("$name cannot be an empty string"); + } + } + + if (is_array($var)) { + if (strlen(implode("", $var)) === 0) { + throw new InvalidArgumentException("$name cannot be an array of empty strings"); + } + } + } + + /** + * @param $endpoint AbstractEndpoint + * + * @throws \Exception + * @return array + */ + private function performRequest(AbstractEndpoint $endpoint) + { + $promise = $this->transport->performRequest( + $endpoint->getMethod(), + $endpoint->getURI(), + $endpoint->getParams(), + $endpoint->getBody(), + $endpoint->getOptions() + ); + + return $this->transport->resultOrFuture($promise, $endpoint->getOptions()); + } +} diff --git a/Framework/lib/Elasticsearch/ClientBuilder.php b/Framework/lib/Elasticsearch/ClientBuilder.php new file mode 100755 index 0000000..7554e36 --- /dev/null +++ b/Framework/lib/Elasticsearch/ClientBuilder.php @@ -0,0 +1,617 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ClientBuilder +{ + /** @var Transport */ + private $transport; + + /** @var callback */ + private $endpoint; + + /** @var NamespaceBuilderInterface[] */ + private $registeredNamespacesBuilders = []; + + /** @var ConnectionFactoryInterface */ + private $connectionFactory; + + private $handler; + + /** @var LoggerInterface */ + private $logger; + + /** @var LoggerInterface */ + private $tracer; + + /** @var string */ + private $connectionPool = '\Elasticsearch\ConnectionPool\StaticNoPingConnectionPool'; + + /** @var string */ + private $serializer = '\Elasticsearch\Serializers\SmartSerializer'; + + /** @var string */ + private $selector = '\Elasticsearch\ConnectionPool\Selectors\RoundRobinSelector'; + + /** @var array */ + private $connectionPoolArgs = [ + 'randomizeHosts' => true + ]; + + /** @var array */ + private $hosts; + + /** @var int */ + private $retries; + + /** @var bool */ + private $sniffOnStart = false; + + /** @var null|array */ + private $sslCert = null; + + /** @var null|array */ + private $sslKey = null; + + /** @var null|bool|string */ + private $sslVerification = null; + + /** + * @return ClientBuilder + */ + public static function create() + { + return new static(); + } + + /** + * Build a new client from the provided config. Hash keys + * should correspond to the method name e.g. ['connectionPool'] + * corresponds to setConnectionPool(). + * + * Missing keys will use the default for that setting if applicable + * + * Unknown keys will throw an exception by default, but this can be silenced + * by setting `quiet` to true + * + * @param array $config hash of settings + * @param bool $quiet False if unknown settings throw exception, true to silently + * ignore unknown settings + * @throws Common\Exceptions\RuntimeException + * @return \Elasticsearch\Client + */ + public static function fromConfig($config, $quiet = false) + { + $builder = new self; + foreach ($config as $key => $value) { + $method = "set$key"; + if (method_exists($builder, $method)) { + $builder->$method($value); + unset($config[$key]); + } + } + + if ($quiet === false && count($config) > 0) { + $unknown = implode(array_keys($config)); + throw new RuntimeException("Unknown parameters provided: $unknown"); + } + return $builder->build(); + } + + /** + * @param array $singleParams + * @param array $multiParams + * @throws \RuntimeException + * @return callable + */ + public static function defaultHandler($multiParams = [], $singleParams = []) + { + $future = null; + if (extension_loaded('curl')) { + $config = array_merge([ 'mh' => curl_multi_init() ], $multiParams); + if (function_exists('curl_reset')) { + $default = new CurlHandler($singleParams); + $future = new CurlMultiHandler($config); + } else { + $default = new CurlMultiHandler($config); + } + } else { + throw new \RuntimeException('Elasticsearch-PHP requires cURL, or a custom HTTP handler.'); + } + + return $future ? Middleware::wrapFuture($default, $future) : $default; + } + + /** + * @param array $params + * @throws \RuntimeException + * @return CurlMultiHandler + */ + public static function multiHandler($params = []) + { + if (function_exists('curl_multi_init')) { + return new CurlMultiHandler(array_merge([ 'mh' => curl_multi_init() ], $params)); + } else { + throw new \RuntimeException('CurlMulti handler requires cURL.'); + } + } + + /** + * @return CurlHandler + * @throws \RuntimeException + */ + public static function singleHandler() + { + if (function_exists('curl_reset')) { + return new CurlHandler(); + } else { + throw new \RuntimeException('CurlSingle handler requires cURL.'); + } + } + + /** + * @param $path string + * @return \Monolog\Logger\Logger + */ + public static function defaultLogger($path, $level = Logger::WARNING) + { + $log = new Logger('log'); + $handler = new StreamHandler($path, $level); + $log->pushHandler($handler); + + return $log; + } + + /** + * @param \Elasticsearch\Connections\ConnectionFactoryInterface $connectionFactory + * @return $this + */ + public function setConnectionFactory(ConnectionFactoryInterface $connectionFactory) + { + $this->connectionFactory = $connectionFactory; + + return $this; + } + + /** + * @param \Elasticsearch\ConnectionPool\AbstractConnectionPool|string $connectionPool + * @param array $args + * @throws \InvalidArgumentException + * @return $this + */ + public function setConnectionPool($connectionPool, array $args = []) + { + if (is_string($connectionPool)) { + $this->connectionPool = $connectionPool; + $this->connectionPoolArgs = $args; + } elseif (is_object($connectionPool)) { + $this->connectionPool = $connectionPool; + } else { + throw new InvalidArgumentException("Serializer must be a class path or instantiated object extending AbstractConnectionPool"); + } + + return $this; + } + + /** + * @param callable $endpoint + * @return $this + */ + public function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } + + /** + * @param NamespaceBuilderInterface $namespaceBuilder + * @return $this + */ + public function registerNamespace(NamespaceBuilderInterface $namespaceBuilder) + { + $this->registeredNamespacesBuilders[] = $namespaceBuilder; + + return $this; + } + + /** + * @param \Elasticsearch\Transport $transport + * @return $this + */ + public function setTransport($transport) + { + $this->transport = $transport; + + return $this; + } + + /** + * @param mixed $handler + * @return $this + */ + public function setHandler($handler) + { + $this->handler = $handler; + + return $this; + } + + /** + * @param \Psr\Log\LoggerInterface $logger + * @return $this + */ + public function setLogger($logger) + { + $this->logger = $logger; + + return $this; + } + + /** + * @param \Psr\Log\LoggerInterface $tracer + * @return $this + */ + public function setTracer($tracer) + { + $this->tracer = $tracer; + + return $this; + } + + /** + * @param \Elasticsearch\Serializers\SerializerInterface|string $serializer + * @throws \InvalidArgumentException + * @return $this + */ + public function setSerializer($serializer) + { + $this->parseStringOrObject($serializer, $this->serializer, 'SerializerInterface'); + + return $this; + } + + /** + * @param array $hosts + * @return $this + */ + public function setHosts($hosts) + { + $this->hosts = $hosts; + + return $this; + } + + /** + * @param int $retries + * @return $this + */ + public function setRetries($retries) + { + $this->retries = $retries; + + return $this; + } + + /** + * @param \Elasticsearch\ConnectionPool\Selectors\SelectorInterface|string $selector + * @throws \InvalidArgumentException + * @return $this + */ + public function setSelector($selector) + { + $this->parseStringOrObject($selector, $this->selector, 'SelectorInterface'); + + return $this; + } + + /** + * @param boolean $sniffOnStart + * @return $this + */ + public function setSniffOnStart($sniffOnStart) + { + $this->sniffOnStart = $sniffOnStart; + + return $this; + } + + /** + * @param $cert + * @param null|string $password + * @return $this + */ + public function setSSLCert($cert, $password = null) + { + $this->sslCert = [$cert, $password]; + + return $this; + } + + /** + * @param $key + * @param null|string $password + * @return $this + */ + public function setSSLKey($key, $password = null) + { + $this->sslKey = [$key, $password]; + + return $this; + } + + /** + * @param bool|string $value + * @return $this + */ + public function setSSLVerification($value = true) + { + $this->sslVerification = $value; + + return $this; + } + + /** + * @return Client + */ + public function build() + { + $this->buildLoggers(); + + if (is_null($this->handler)) { + $this->handler = ClientBuilder::defaultHandler(); + } + + $sslOptions = null; + if (isset($this->sslKey)) { + $sslOptions['ssl_key'] = $this->sslKey; + } + if (isset($this->sslCert)) { + $sslOptions['cert'] = $this->sslCert; + } + if (isset($this->sslVerification)) { + $sslOptions['verify'] = $this->sslVerification; + } + + if (!is_null($sslOptions)) { + $sslHandler = function (callable $handler, array $sslOptions) { + return function (array $request) use ($handler, $sslOptions) { + // Add our custom headers + foreach ($sslOptions as $key => $value) { + $request['client'][$key] = $value; + } + + // Send the request using the handler and return the response. + return $handler($request); + }; + }; + $this->handler = $sslHandler($this->handler, $sslOptions); + } + + if (is_null($this->serializer)) { + $this->serializer = new SmartSerializer(); + } elseif (is_string($this->serializer)) { + $this->serializer = new $this->serializer; + } + + if (is_null($this->connectionFactory)) { + $connectionParams = []; + $this->connectionFactory = new ConnectionFactory($this->handler, $connectionParams, $this->serializer, $this->logger, $this->tracer); + } + + if (is_null($this->hosts)) { + $this->hosts = $this->getDefaultHost(); + } + + if (is_null($this->selector)) { + $this->selector = new Selectors\RoundRobinSelector(); + } elseif (is_string($this->selector)) { + $this->selector = new $this->selector; + } + + $this->buildTransport(); + + if (is_null($this->endpoint)) { + $serializer = $this->serializer; + + $this->endpoint = function ($class) use ($serializer) { + $fullPath = '\\Elasticsearch\\Endpoints\\' . $class; + if ($class === 'Bulk' || $class === 'Msearch' || $class === 'MPercolate') { + return new $fullPath($serializer); + } else { + return new $fullPath(); + } + }; + } + + $registeredNamespaces = []; + foreach ($this->registeredNamespacesBuilders as $builder) { + /** @var $builder NamespaceBuilderInterface */ + $registeredNamespaces[$builder->getName()] = $builder->getObject($this->transport, $this->serializer); + } + + return $this->instantiate($this->transport, $this->endpoint, $registeredNamespaces); + } + + /** + * @param Transport $transport + * @param callable $endpoint + * @param Object[] $registeredNamespaces + * @return Client + */ + protected function instantiate(Transport $transport, callable $endpoint, array $registeredNamespaces) + { + return new Client($transport, $endpoint, $registeredNamespaces); + } + + private function buildLoggers() + { + if (is_null($this->logger)) { + $this->logger = new NullLogger(); + } + + if (is_null($this->tracer)) { + $this->tracer = new NullLogger(); + } + } + + private function buildTransport() + { + $connections = $this->buildConnectionsFromHosts($this->hosts); + + if (is_string($this->connectionPool)) { + $this->connectionPool = new $this->connectionPool( + $connections, + $this->selector, + $this->connectionFactory, + $this->connectionPoolArgs); + } elseif (is_null($this->connectionPool)) { + $this->connectionPool = new StaticNoPingConnectionPool( + $connections, + $this->selector, + $this->connectionFactory, + $this->connectionPoolArgs); + } + + if (is_null($this->retries)) { + $this->retries = count($connections); + } + + if (is_null($this->transport)) { + $this->transport = new Transport($this->retries, $this->sniffOnStart, $this->connectionPool, $this->logger); + } + } + + private function parseStringOrObject($arg, &$destination, $interface) + { + if (is_string($arg)) { + $destination = new $arg; + } elseif (is_object($arg)) { + $destination = $arg; + } else { + throw new InvalidArgumentException("Serializer must be a class path or instantiated object implementing $interface"); + } + } + + /** + * @return array + */ + private function getDefaultHost() + { + return ['localhost:9200']; + } + + /** + * @param array $hosts + * + * @throws \InvalidArgumentException + * @return \Elasticsearch\Connections\Connection[] + */ + private function buildConnectionsFromHosts($hosts) + { + if (is_array($hosts) === false) { + $this->logger->error("Hosts parameter must be an array of strings, or an array of Connection hashes."); + throw new InvalidArgumentException('Hosts parameter must be an array of strings, or an array of Connection hashes.'); + } + + $connections = []; + foreach ($hosts as $host) { + if (is_string($host)) { + $host = $this->prependMissingScheme($host); + $host = $this->extractURIParts($host); + } else if (is_array($host)) { + $host = $this->normalizeExtendedHost($host); + } else { + $this->logger->error("Could not parse host: ".print_r($host, true)); + throw new RuntimeException("Could not parse host: ".print_r($host, true)); + } + $connections[] = $this->connectionFactory->create($host); + } + + return $connections; + } + + /** + * @param $host + * @return array + */ + private function normalizeExtendedHost($host) { + if (isset($host['host']) === false) { + $this->logger->error("Required 'host' was not defined in extended format: ".print_r($host, true)); + throw new RuntimeException("Required 'host' was not defined in extended format: ".print_r($host, true)); + } + + if (isset($host['scheme']) === false) { + $host['scheme'] = 'http'; + } + if (isset($host['port']) === false) { + $host['port'] = '9200'; + } + return $host; + } + + /** + * @param array $host + * + * @throws \InvalidArgumentException + * @return array + */ + private function extractURIParts($host) + { + $parts = parse_url($host); + + if ($parts === false) { + throw new InvalidArgumentException("Could not parse URI"); + } + + if (isset($parts['port']) !== true) { + $parts['port'] = 9200; + } + + return $parts; + } + + /** + * @param string $host + * + * @return string + */ + private function prependMissingScheme($host) + { + if (!filter_var($host, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED)) { + $host = 'http://' . $host; + } + + return $host; + } +} diff --git a/Framework/lib/Elasticsearch/Common/EmptyLogger.php b/Framework/lib/Elasticsearch/Common/EmptyLogger.php new file mode 100755 index 0000000..89ced83 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/EmptyLogger.php @@ -0,0 +1,35 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class EmptyLogger extends AbstractLogger implements LoggerInterface +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return null + */ + public function log($level, $message, array $context = array()) + { + return; + } +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/AlreadyExpiredException.php b/Framework/lib/Elasticsearch/Common/Exceptions/AlreadyExpiredException.php new file mode 100755 index 0000000..411c70a --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/AlreadyExpiredException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class AlreadyExpiredException extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/BadMethodCallException.php b/Framework/lib/Elasticsearch/Common/Exceptions/BadMethodCallException.php new file mode 100755 index 0000000..d8dea6c --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/BadMethodCallException.php @@ -0,0 +1,18 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class BadMethodCallException extends \BadMethodCallException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/BadRequest400Exception.php b/Framework/lib/Elasticsearch/Common/Exceptions/BadRequest400Exception.php new file mode 100755 index 0000000..1c652d7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/BadRequest400Exception.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class BadRequest400Exception extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/ClientErrorResponseException.php b/Framework/lib/Elasticsearch/Common/Exceptions/ClientErrorResponseException.php new file mode 100755 index 0000000..844bbcc --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/ClientErrorResponseException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ClientErrorResponseException extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Conflict409Exception.php b/Framework/lib/Elasticsearch/Common/Exceptions/Conflict409Exception.php new file mode 100755 index 0000000..d7f10a4 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Conflict409Exception.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Conflict409Exception extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotConnectToHost.php b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotConnectToHost.php new file mode 100755 index 0000000..b1ccc22 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotConnectToHost.php @@ -0,0 +1,19 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class CouldNotConnectToHost extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotResolveHostException.php b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotResolveHostException.php new file mode 100755 index 0000000..283afdf --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/CouldNotResolveHostException.php @@ -0,0 +1,19 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class CouldNotResolveHostException extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Curl/OperationTimeoutException.php b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/OperationTimeoutException.php new file mode 100755 index 0000000..12c1722 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Curl/OperationTimeoutException.php @@ -0,0 +1,19 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class OperationTimeoutException extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/ElasticsearchException.php b/Framework/lib/Elasticsearch/Common/Exceptions/ElasticsearchException.php new file mode 100755 index 0000000..a5cab88 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/ElasticsearchException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Forbidden403Exception.php b/Framework/lib/Elasticsearch/Common/Exceptions/Forbidden403Exception.php new file mode 100755 index 0000000..2b84c64 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Forbidden403Exception.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Forbidden403Exception extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/InvalidArgumentException.php b/Framework/lib/Elasticsearch/Common/Exceptions/InvalidArgumentException.php new file mode 100755 index 0000000..65e932b --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/InvalidArgumentException.php @@ -0,0 +1,18 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class InvalidArgumentException extends \InvalidArgumentException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/MaxRetriesException.php b/Framework/lib/Elasticsearch/Common/Exceptions/MaxRetriesException.php new file mode 100755 index 0000000..15b2833 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/MaxRetriesException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class MaxRetriesException extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Missing404Exception.php b/Framework/lib/Elasticsearch/Common/Exceptions/Missing404Exception.php new file mode 100755 index 0000000..76bc87a --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Missing404Exception.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Missing404Exception extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/NoDocumentsToGetException.php b/Framework/lib/Elasticsearch/Common/Exceptions/NoDocumentsToGetException.php new file mode 100755 index 0000000..75beb9f --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/NoDocumentsToGetException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class NoDocumentsToGetException extends ServerErrorResponseException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/NoNodesAvailableException.php b/Framework/lib/Elasticsearch/Common/Exceptions/NoNodesAvailableException.php new file mode 100755 index 0000000..63a1793 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/NoNodesAvailableException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class NoNodesAvailableException extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/NoShardAvailableException.php b/Framework/lib/Elasticsearch/Common/Exceptions/NoShardAvailableException.php new file mode 100755 index 0000000..71b9a4a --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/NoShardAvailableException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class NoShardAvailableException extends ServerErrorResponseException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/RequestTimeout408Exception.php b/Framework/lib/Elasticsearch/Common/Exceptions/RequestTimeout408Exception.php new file mode 100755 index 0000000..8b668a8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/RequestTimeout408Exception.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class RequestTimeout408Exception extends BadRequest400Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/RoutingMissingException.php b/Framework/lib/Elasticsearch/Common/Exceptions/RoutingMissingException.php new file mode 100755 index 0000000..efa3cbd --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/RoutingMissingException.php @@ -0,0 +1,17 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class RoutingMissingException extends ServerErrorResponseException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/RuntimeException.php b/Framework/lib/Elasticsearch/Common/Exceptions/RuntimeException.php new file mode 100755 index 0000000..2fc381a --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/RuntimeException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class RuntimeException extends \RuntimeException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/ScriptLangNotSupportedException.php b/Framework/lib/Elasticsearch/Common/Exceptions/ScriptLangNotSupportedException.php new file mode 100755 index 0000000..255c3a5 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/ScriptLangNotSupportedException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ScriptLangNotSupportedException extends BadRequest400Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/Serializer/JsonErrorException.php b/Framework/lib/Elasticsearch/Common/Exceptions/Serializer/JsonErrorException.php new file mode 100755 index 0000000..626d4ae --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/Serializer/JsonErrorException.php @@ -0,0 +1,69 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class JsonErrorException extends \Exception implements ElasticsearchException +{ + /** + * @var mixed + */ + private $input; + + /** + * @var mixed + */ + private $result; + + private static $messages = array( + JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded', + JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', + JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', + JSON_ERROR_SYNTAX => 'Syntax error', + JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', + JSON_ERROR_RECURSION => 'One or more recursive references in the value to be encoded', + JSON_ERROR_INF_OR_NAN => 'One or more NAN or INF values in the value to be encoded', + JSON_ERROR_UNSUPPORTED_TYPE => 'A value of a type that cannot be encoded was given', + + // JSON_ERROR_* constant values that are available on PHP >= 7.0 + 9 => 'Decoding of value would result in invalid PHP property name', //JSON_ERROR_INVALID_PROPERTY_NAME + 10 => 'Attempted to decode nonexistent UTF-16 code-point' //JSON_ERROR_UTF16 + ); + + public function __construct($code, $input, $result, $previous = null) + { + if (isset(self::$messages[$code]) !== true) { + throw new \InvalidArgumentException(sprintf('Encountered unknown JSON error code: [%d]', $code)); + } + + parent::__construct(self::$messages[$code], $code, $previous); + $this->input = $input; + $this->result = $result; + } + + /** + * @return mixed + */ + public function getInput() + { + return $this->input; + } + + /** + * @return mixed + */ + public function getResult() + { + return $this->result; + } +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/ServerErrorResponseException.php b/Framework/lib/Elasticsearch/Common/Exceptions/ServerErrorResponseException.php new file mode 100755 index 0000000..9841254 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/ServerErrorResponseException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ServerErrorResponseException extends TransportException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/TransportException.php b/Framework/lib/Elasticsearch/Common/Exceptions/TransportException.php new file mode 100755 index 0000000..6dce5b4 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/TransportException.php @@ -0,0 +1,16 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class TransportException extends \Exception implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/Common/Exceptions/UnexpectedValueException.php b/Framework/lib/Elasticsearch/Common/Exceptions/UnexpectedValueException.php new file mode 100755 index 0000000..2a63e80 --- /dev/null +++ b/Framework/lib/Elasticsearch/Common/Exceptions/UnexpectedValueException.php @@ -0,0 +1,18 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class UnexpectedValueException extends \UnexpectedValueException implements ElasticsearchException +{ +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/AbstractConnectionPool.php b/Framework/lib/Elasticsearch/ConnectionPool/AbstractConnectionPool.php new file mode 100755 index 0000000..625eaa4 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/AbstractConnectionPool.php @@ -0,0 +1,86 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractConnectionPool implements ConnectionPoolInterface +{ + /** + * Array of connections + * + * @var ConnectionInterface[] + */ + protected $connections; + + /** + * Array of initial seed connections + * + * @var ConnectionInterface[] + */ + protected $seedConnections; + + /** + * Selector object, used to select a connection on each request + * + * @var SelectorInterface + */ + protected $selector; + + /** @var array */ + protected $connectionPoolParams; + + /** @var \Elasticsearch\Connections\ConnectionFactory */ + protected $connectionFactory; + + /** + * Constructor + * + * @param ConnectionInterface[] $connections The Connections to choose from + * @param SelectorInterface $selector A Selector instance to perform the selection logic for the available connections + * @param ConnectionFactoryInterface $factory ConnectionFactory instance + * @param array $connectionPoolParams + */ + public function __construct($connections, SelectorInterface $selector, ConnectionFactoryInterface $factory, $connectionPoolParams) + { + $paramList = array('connections', 'selector', 'connectionPoolParams'); + foreach ($paramList as $param) { + if (isset($$param) === false) { + throw new InvalidArgumentException('`' . $param . '` parameter must not be null'); + } + } + + if (isset($connectionPoolParams['randomizeHosts']) === true + && $connectionPoolParams['randomizeHosts'] === true) { + shuffle($connections); + } + + $this->connections = $connections; + $this->seedConnections = $connections; + $this->selector = $selector; + $this->connectionPoolParams = $connectionPoolParams; + $this->connectionFactory = $factory; + } + + /** + * @param bool $force + * + * @return Connection + */ + abstract public function nextConnection($force = false); + + abstract public function scheduleCheck(); +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/ConnectionPoolInterface.php b/Framework/lib/Elasticsearch/ConnectionPool/ConnectionPoolInterface.php new file mode 100755 index 0000000..d10fc35 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/ConnectionPoolInterface.php @@ -0,0 +1,29 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface ConnectionPoolInterface +{ + /** + * @param bool $force + * + * @return ConnectionInterface + */ + public function nextConnection($force = false); + + /** + * @return void + */ + public function scheduleCheck(); +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RandomSelector.php b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RandomSelector.php new file mode 100755 index 0000000..b544292 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RandomSelector.php @@ -0,0 +1,29 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class RandomSelector implements SelectorInterface +{ + /** + * Select a random connection from the provided array + * + * @param ConnectionInterface[] $connections an array of ConnectionInterface instances to choose from + * + * @return \Elasticsearch\Connections\ConnectionInterface + */ + public function select($connections) + { + return $connections[array_rand($connections)]; + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RoundRobinSelector.php b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RoundRobinSelector.php new file mode 100755 index 0000000..e8b9784 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/RoundRobinSelector.php @@ -0,0 +1,36 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class RoundRobinSelector implements SelectorInterface +{ + /** + * @var int + */ + private $current = 0; + + /** + * Select the next connection in the sequence + * + * @param ConnectionInterface[] $connections an array of ConnectionInterface instances to choose from + * + * @return \Elasticsearch\Connections\ConnectionInterface + */ + public function select($connections) + { + $this->current += 1; + + return $connections[$this->current % count($connections)]; + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/Selectors/SelectorInterface.php b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/SelectorInterface.php new file mode 100755 index 0000000..72dfd19 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/SelectorInterface.php @@ -0,0 +1,24 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface SelectorInterface +{ + /** + * Perform logic to select a single ConnectionInterface instance from the array provided + * + * @param ConnectionInterface[] $connections an array of ConnectionInterface instances to choose from + * + * @return \Elasticsearch\Connections\ConnectionInterface + */ + public function select($connections); +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/Selectors/StickyRoundRobinSelector.php b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/StickyRoundRobinSelector.php new file mode 100755 index 0000000..f44a68c --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/Selectors/StickyRoundRobinSelector.php @@ -0,0 +1,47 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class StickyRoundRobinSelector implements SelectorInterface +{ + /** + * @var int + */ + private $current = 0; + + /** + * @var int + */ + private $currentCounter = 0; + + /** + * Use current connection unless it is dead, otherwise round-robin + * + * @param ConnectionInterface[] $connections Array of connections to choose from + * + * @return ConnectionInterface + */ + public function select($connections) + { + /** @var ConnectionInterface[] $connections */ + if ($connections[$this->current]->isAlive()) { + return $connections[$this->current]; + } + + $this->currentCounter += 1; + $this->current = $this->currentCounter % count($connections); + + return $connections[$this->current]; + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/SimpleConnectionPool.php b/Framework/lib/Elasticsearch/ConnectionPool/SimpleConnectionPool.php new file mode 100755 index 0000000..f77e8d7 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/SimpleConnectionPool.php @@ -0,0 +1,34 @@ +selector->select($this->connections); + } + + public function scheduleCheck() + { + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/SniffingConnectionPool.php b/Framework/lib/Elasticsearch/ConnectionPool/SniffingConnectionPool.php new file mode 100755 index 0000000..89b1f81 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/SniffingConnectionPool.php @@ -0,0 +1,155 @@ +setConnectionPoolParams($connectionPoolParams); + $this->nextSniff = time() + $this->sniffingInterval; + } + + /** + * @param bool $force + * + * @return Connection + * @throws \Elasticsearch\Common\Exceptions\NoNodesAvailableException + */ + public function nextConnection($force = false) + { + $this->sniff($force); + + $size = count($this->connections); + while ($size--) { + /** @var Connection $connection */ + $connection = $this->selector->select($this->connections); + if ($connection->isAlive() === true || $connection->ping() === true) { + return $connection; + } + } + + if ($force === true) { + throw new NoNodesAvailableException("No alive nodes found in your cluster"); + } + + return $this->nextConnection(true); + } + + public function scheduleCheck() + { + $this->nextSniff = -1; + } + + /** + * @param bool $force + */ + private function sniff($force = false) + { + if ($force === false && $this->nextSniff >= time()) { + return; + } + + $total = count($this->connections); + + while ($total--) { + /** @var Connection $connection */ + $connection = $this->selector->select($this->connections); + + if ($connection->isAlive() xor $force) { + continue; + } + + if ($this->sniffConnection($connection) === true) { + return; + } + } + + if ($force === true) { + return; + } + + foreach ($this->seedConnections as $connection) { + if ($this->sniffConnection($connection) === true) { + return; + } + } + } + + /** + * @param Connection $connection + * @return bool + */ + private function sniffConnection(Connection $connection) + { + try { + $response = $connection->sniff(); + } catch (OperationTimeoutException $exception) { + return false; + } + + $nodes = $this->parseClusterState($connection->getTransportSchema(), $response); + + if (count($nodes) === 0) { + return false; + } + + $this->connections = array(); + + foreach ($nodes as $node) { + $nodeDetails = array( + 'host' => $node['host'], + 'port' => $node['port'] + ); + $this->connections[] = $this->connectionFactory->create($nodeDetails); + } + + $this->nextSniff = time() + $this->sniffingInterval; + + return true; + } + + private function parseClusterState($transportSchema, $nodeInfo) + { + $pattern = '/\/([^:]*):([0-9]+)\]/'; + $schemaAddress = $transportSchema . '_address'; + $hosts = array(); + + foreach ($nodeInfo['nodes'] as $node) { + if (isset($node[$schemaAddress]) === true) { + if (preg_match($pattern, $node[$schemaAddress], $match) === 1) { + $hosts[] = array( + 'host' => $match[1], + 'port' => (int) $match[2], + ); + } + } + } + + return $hosts; + } + + private function setConnectionPoolParams($connectionPoolParams) + { + if (isset($connectionPoolParams['sniffingInterval']) === true) { + $this->sniffingInterval = $connectionPoolParams['sniffingInterval']; + } + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/StaticConnectionPool.php b/Framework/lib/Elasticsearch/ConnectionPool/StaticConnectionPool.php new file mode 100755 index 0000000..102dda3 --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/StaticConnectionPool.php @@ -0,0 +1,93 @@ +scheduleCheck(); + } + + /** + * @param bool $force + * + * @return Connection + * @throws \Elasticsearch\Common\Exceptions\NoNodesAvailableException + */ + public function nextConnection($force = false) + { + $skipped = array(); + + $total = count($this->connections); + while ($total--) { + /** @var Connection $connection */ + $connection = $this->selector->select($this->connections); + if ($connection->isAlive() === true) { + return $connection; + } + + if ($this->readyToRevive($connection) === true) { + if ($connection->ping() === true) { + return $connection; + } + } else { + $skipped[] = $connection; + } + } + + // All "alive" nodes failed, force pings on "dead" nodes + foreach ($skipped as $connection) { + if ($connection->ping() === true) { + return $connection; + } + } + + throw new NoNodesAvailableException("No alive nodes found in your cluster"); + } + + public function scheduleCheck() + { + foreach ($this->connections as $connection) { + $connection->markDead(); + } + } + + /** + * @param Connection $connection + * + * @return bool + */ + private function readyToRevive(Connection $connection) + { + $timeout = min( + $this->pingTimeout * pow(2, $connection->getPingFailures()), + $this->maxPingTimeout + ); + + if ($connection->getLastPing() + $timeout < time()) { + return true; + } else { + return false; + } + } +} diff --git a/Framework/lib/Elasticsearch/ConnectionPool/StaticNoPingConnectionPool.php b/Framework/lib/Elasticsearch/ConnectionPool/StaticNoPingConnectionPool.php new file mode 100755 index 0000000..b7b056e --- /dev/null +++ b/Framework/lib/Elasticsearch/ConnectionPool/StaticNoPingConnectionPool.php @@ -0,0 +1,76 @@ +connections); + while ($total--) { + /** @var Connection $connection */ + $connection = $this->selector->select($this->connections); + if ($connection->isAlive() === true) { + return $connection; + } + + if ($this->readyToRevive($connection) === true) { + return $connection; + } + } + + throw new NoNodesAvailableException("No alive nodes found in your cluster"); + } + + public function scheduleCheck() + { + } + + /** + * @param \Elasticsearch\Connections\Connection $connection + * + * @return bool + */ + private function readyToRevive(Connection $connection) + { + $timeout = min( + $this->pingTimeout * pow(2, $connection->getPingFailures()), + $this->maxPingTimeout + ); + + if ($connection->getLastPing() + $timeout < time()) { + return true; + } else { + return false; + } + } +} diff --git a/Framework/lib/Elasticsearch/Connections/Connection.php b/Framework/lib/Elasticsearch/Connections/Connection.php new file mode 100755 index 0000000..2d9e289 --- /dev/null +++ b/Framework/lib/Elasticsearch/Connections/Connection.php @@ -0,0 +1,706 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Connection implements ConnectionInterface +{ + /** @var callable */ + protected $handler; + + /** @var SerializerInterface */ + protected $serializer; + + /** + * @var string + */ + protected $transportSchema = 'http'; // TODO depreciate this default + + /** + * @var string + */ + protected $host; + + /** + * @var string || null + */ + protected $path; + + /** + * @var LoggerInterface + */ + protected $log; + + /** + * @var LoggerInterface + */ + protected $trace; + + /** + * @var array + */ + protected $connectionParams; + + /** @var bool */ + protected $isAlive = false; + + /** @var float */ + private $pingTimeout = 1; //TODO expose this + + /** @var int */ + private $lastPing = 0; + + /** @var int */ + private $failedPings = 0; + + private $lastRequest = array(); + + /** + * Constructor + * + * @param $handler + * @param array $hostDetails + * @param array $connectionParams Array of connection-specific parameters + * @param \Elasticsearch\Serializers\SerializerInterface $serializer + * @param \Psr\Log\LoggerInterface $log Logger object + * @param \Psr\Log\LoggerInterface $trace + */ + public function __construct($handler, $hostDetails, $connectionParams, + SerializerInterface $serializer, LoggerInterface $log, LoggerInterface $trace) + { + if (isset($hostDetails['port']) !== true) { + $hostDetails['port'] = 9200; + } + + if (isset($hostDetails['scheme'])) { + $this->transportSchema = $hostDetails['scheme']; + } + + if (isset($hostDetails['user']) && isset($hostDetails['pass'])) { + $connectionParams['client']['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; + $connectionParams['client']['curl'][CURLOPT_USERPWD] = $hostDetails['user'].':'.$hostDetails['pass']; + } + + $host = $hostDetails['host'].':'.$hostDetails['port']; + $path = null; + if (isset($hostDetails['path']) === true) { + $path = $hostDetails['path']; + } + $this->host = $host; + $this->path = $path; + $this->log = $log; + $this->trace = $trace; + $this->connectionParams = $connectionParams; + $this->serializer = $serializer; + + $this->handler = $this->wrapHandler($handler, $log, $trace); + } + + /** + * @param $method + * @param $uri + * @param null $params + * @param null $body + * @param array $options + * @param \Elasticsearch\Transport $transport + * @return mixed + */ + public function performRequest($method, $uri, $params = null, $body = null, $options = [], Transport $transport = null) + { + if (isset($body) === true) { + $body = $this->serializer->serialize($body); + } + + $request = [ + 'http_method' => $method, + 'scheme' => $this->transportSchema, + 'uri' => $this->getURI($uri, $params), + 'body' => $body, + 'headers' => [ + 'host' => [$this->host] + ] + + ]; + $request = array_merge_recursive($request, $this->connectionParams, $options); + + + $handler = $this->handler; + $future = $handler($request, $this, $transport, $options); + + return $future; + } + + /** @return string */ + public function getTransportSchema() + { + return $this->transportSchema; + } + + /** @return array */ + public function getLastRequestInfo() + { + return $this->lastRequest; + } + + private function wrapHandler(callable $handler, LoggerInterface $logger, LoggerInterface $tracer) + { + return function (array $request, Connection $connection, Transport $transport = null, $options) use ($handler, $logger, $tracer) { + + $this->lastRequest = []; + $this->lastRequest['request'] = $request; + + // Send the request using the wrapped handler. + $response = Core::proxy($handler($request), function ($response) use ($connection, $transport, $logger, $tracer, $request, $options) { + + $this->lastRequest['response'] = $response; + + if (isset($response['error']) === true) { + if ($response['error'] instanceof ConnectException || $response['error'] instanceof RingException) { + $this->log->warning("Curl exception encountered."); + + $exception = $this->getCurlRetryException($request, $response); + + $this->logRequestFail( + $request['http_method'], + $response['effective_url'], + $request['body'], + $request['headers'], + $response['status'], + $response['body'], + $response['transfer_stats']['total_time'], + $exception + ); + + $node = $connection->getHost(); + $this->log->warning("Marking node $node dead."); + $connection->markDead(); + + // If the transport has not been set, we are inside a Ping or Sniff, + // so we don't want to retrigger retries anyway. + // + // TODO this could be handled better, but we are limited because connectionpools do not + // have access to Transport. Architecturally, all of this needs to be refactored + if (isset($transport) === true) { + $transport->connectionPool->scheduleCheck(); + + $neverRetry = isset($request['client']['never_retry']) ? $request['client']['never_retry'] : false; + $shouldRetry = $transport->shouldRetry($request); + $shouldRetryText = ($shouldRetry) ? 'true' : 'false'; + + $this->log->warning("Retries left? $shouldRetryText"); + if ($shouldRetry && !$neverRetry) { + return $transport->performRequest( + $request['http_method'], + $request['uri'], + [], + $request['body'], + $options + ); + } + } + + $this->log->warning("Out of retries, throwing exception from $node"); + // Only throw if we run out of retries + throw $exception; + } else { + // Something went seriously wrong, bail + $exception = new TransportException($response['error']->getMessage()); + $this->logRequestFail( + $request['http_method'], + $response['effective_url'], + $request['body'], + $request['headers'], + $response['status'], + $response['body'], + $response['transfer_stats']['total_time'], + $exception + ); + throw $exception; + } + } else { + $connection->markAlive(); + + if (isset($response['body']) === true) { + $response['body'] = stream_get_contents($response['body']); + $this->lastRequest['response']['body'] = $response['body']; + } + + if ($response['status'] >= 400 && $response['status'] < 500) { + $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; + $this->process4xxError($request, $response, $ignore); + } elseif ($response['status'] >= 500) { + $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; + $this->process5xxError($request, $response, $ignore); + } + + // No error, deserialize + $response['body'] = $this->serializer->deserialize($response['body'], $response['transfer_stats']); + } + $this->logRequestSuccess( + $request['http_method'], + $response['effective_url'], + $request['body'], + $request['headers'], + $response['status'], + $response['body'], + $response['transfer_stats']['total_time'] + ); + + return isset($request['client']['verbose']) && $request['client']['verbose'] === true ? $response : $response['body']; + + }); + + return $response; + }; + } + + /** + * @param string $uri + * @param array $params + * + * @return string + */ + private function getURI($uri, $params) + { + if (isset($params) === true && !empty($params)) { + array_walk($params, function (&$value, &$key) { + if ($value === true) { + $value = 'true'; + } else if ($value === false) { + $value = 'false'; + } + }); + + $uri .= '?' . http_build_query($params); + } + + if ($this->path !== null) { + $uri = $this->path . $uri; + } + + return $uri; + } + + /** + * Log a successful request + * + * @param string $method + * @param string $fullURI + * @param string $body + * @param array $headers + * @param string $statusCode + * @param string $response + * @param string $duration + * + * @return void + */ + public function logRequestSuccess($method, $fullURI, $body, $headers, $statusCode, $response, $duration) + { + $this->log->debug('Request Body', array($body)); + $this->log->info( + 'Request Success:', + array( + 'method' => $method, + 'uri' => $fullURI, + 'headers' => $headers, + 'HTTP code' => $statusCode, + 'duration' => $duration, + ) + ); + $this->log->debug('Response', array($response)); + + // Build the curl command for Trace. + $curlCommand = $this->buildCurlCommand($method, $fullURI, $body); + $this->trace->info($curlCommand); + $this->trace->debug( + 'Response:', + array( + 'response' => $response, + 'method' => $method, + 'uri' => $fullURI, + 'HTTP code' => $statusCode, + 'duration' => $duration, + ) + ); + } + + /** + * Log a a failed request + * + * @param string $method + * @param string $fullURI + * @param string $body + * @param array $headers + * @param null|string $statusCode + * @param null|string $response + * @param string $duration + * @param \Exception|null $exception + * + * @return void + */ + public function logRequestFail($method, $fullURI, $body, $headers, $statusCode, $response, $duration, \Exception $exception) + { + $this->log->debug('Request Body', array($body)); + $this->log->warning( + 'Request Failure:', + array( + 'method' => $method, + 'uri' => $fullURI, + 'headers' => $headers, + 'HTTP code' => $statusCode, + 'duration' => $duration, + 'error' => $exception->getMessage(), + ) + ); + $this->log->warning('Response', array($response)); + + // Build the curl command for Trace. + $curlCommand = $this->buildCurlCommand($method, $fullURI, $body); + $this->trace->info($curlCommand); + $this->trace->debug( + 'Response:', + array( + 'response' => $response, + 'method' => $method, + 'uri' => $fullURI, + 'HTTP code' => $statusCode, + 'duration' => $duration, + ) + ); + } + + /** + * @return bool + */ + public function ping() + { + $options = [ + 'client' => [ + 'timeout' => $this->pingTimeout, + 'never_retry' => true, + 'verbose' => true + ] + ]; + try { + $response = $this->performRequest('HEAD', '/', null, null, $options); + $response = $response->wait(); + } catch (TransportException $exception) { + $this->markDead(); + + return false; + } + + if ($response['status'] === 200) { + $this->markAlive(); + + return true; + } else { + $this->markDead(); + + return false; + } + } + + /** + * @return array + */ + public function sniff() + { + $options = [ + 'client' => [ + 'timeout' => $this->pingTimeout, + 'never_retry' => true + ] + ]; + + return $this->performRequest('GET', '/_nodes/_all/clear', null, null, $options); + } + + /** + * @return bool + */ + public function isAlive() + { + return $this->isAlive; + } + + public function markAlive() + { + $this->failedPings = 0; + $this->isAlive = true; + $this->lastPing = time(); + } + + public function markDead() + { + $this->isAlive = false; + $this->failedPings += 1; + $this->lastPing = time(); + } + + /** + * @return int + */ + public function getLastPing() + { + return $this->lastPing; + } + + /** + * @return int + */ + public function getPingFailures() + { + return $this->failedPings; + } + + /** + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * @return null|string + */ + public function getUserPass() + { + if (isset($this->connectionParams['client']['curl'][CURLOPT_USERPWD]) === true) { + return $this->connectionParams['client']['curl'][CURLOPT_USERPWD]; + } + return null; + } + + /** + * @return null|string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param $request + * @param $response + * @return \Elasticsearch\Common\Exceptions\Curl\CouldNotConnectToHost|\Elasticsearch\Common\Exceptions\Curl\CouldNotResolveHostException|\Elasticsearch\Common\Exceptions\Curl\OperationTimeoutException|\Elasticsearch\Common\Exceptions\MaxRetriesException + */ + protected function getCurlRetryException($request, $response) + { + $exception = null; + $message = $response['error']->getMessage(); + $exception = new MaxRetriesException($message); + switch ($response['curl']['errno']) { + case 6: + $exception = new CouldNotResolveHostException($message, null, $exception); + break; + case 7: + $exception = new CouldNotConnectToHost($message, null, $exception); + break; + case 28: + $exception = new OperationTimeoutException($message, null, $exception); + break; + } + + return $exception; + } + + /** + * Construct a string cURL command + * + * @param string $method HTTP method + * @param string $uri Full URI of request + * @param string $body Request body + * + * @return string + */ + private function buildCurlCommand($method, $uri, $body) + { + if (strpos($uri, '?') === false) { + $uri .= '?pretty=true'; + } else { + str_replace('?', '?pretty=true', $uri); + } + + $curlCommand = 'curl -X' . strtoupper($method); + $curlCommand .= " '" . $uri . "'"; + + if (isset($body) === true && $body !== '') { + $curlCommand .= " -d '" . $body . "'"; + } + + return $curlCommand; + } + + /** + * @param $request + * @param $response + * @param $ignore + * @throws \Elasticsearch\Common\Exceptions\AlreadyExpiredException|\Elasticsearch\Common\Exceptions\BadRequest400Exception|\Elasticsearch\Common\Exceptions\Conflict409Exception|\Elasticsearch\Common\Exceptions\Forbidden403Exception|\Elasticsearch\Common\Exceptions\Missing404Exception|\Elasticsearch\Common\Exceptions\ScriptLangNotSupportedException|null + */ + private function process4xxError($request, $response, $ignore) + { + $statusCode = $response['status']; + $responseBody = $response['body']; + + /** @var \Exception $exception */ + $exception = $this->tryDeserialize400Error($response); + + if (array_search($response['status'], $ignore) !== false) { + return; + } + + if ($statusCode === 400 && strpos($responseBody, "AlreadyExpiredException") !== false) { + $exception = new AlreadyExpiredException($responseBody, $statusCode); + } elseif ($statusCode === 403) { + $exception = new Forbidden403Exception($responseBody, $statusCode); + } elseif ($statusCode === 404) { + $exception = new Missing404Exception($responseBody, $statusCode); + } elseif ($statusCode === 409) { + $exception = new Conflict409Exception($responseBody, $statusCode); + } elseif ($statusCode === 400 && strpos($responseBody, 'script_lang not supported') !== false) { + $exception = new ScriptLangNotSupportedException($responseBody. $statusCode); + } elseif ($statusCode === 408) { + $exception = new RequestTimeout408Exception($responseBody, $statusCode); + } else { + $exception = new BadRequest400Exception($responseBody, $statusCode); + } + + $this->logRequestFail( + $request['http_method'], + $response['effective_url'], + $request['body'], + $request['headers'], + $response['status'], + $response['body'], + $response['transfer_stats']['total_time'], + $exception + ); + + throw $exception; + } + + /** + * @param $request + * @param $response + * @param $ignore + * @throws \Elasticsearch\Common\Exceptions\NoDocumentsToGetException|\Elasticsearch\Common\Exceptions\NoShardAvailableException|\Elasticsearch\Common\Exceptions\RoutingMissingException|\Elasticsearch\Common\Exceptions\ServerErrorResponseException + */ + private function process5xxError($request, $response, $ignore) + { + $statusCode = $response['status']; + $responseBody = $response['body']; + + /** @var \Exception $exception */ + $exception = $this->tryDeserialize500Error($response); + + $exceptionText = "[$statusCode Server Exception] ".$exception->getMessage(); + $this->log->error($exceptionText); + $this->log->error($exception->getTraceAsString()); + + if (array_search($statusCode, $ignore) !== false) { + return; + } + + if ($statusCode === 500 && strpos($responseBody, "RoutingMissingException") !== false) { + $exception = new RoutingMissingException($exception->getMessage(), $statusCode, $exception); + } elseif ($statusCode === 500 && preg_match('/ActionRequestValidationException.+ no documents to get/', $responseBody) === 1) { + $exception = new NoDocumentsToGetException($exception->getMessage(), $statusCode, $exception); + } elseif ($statusCode === 500 && strpos($responseBody, 'NoShardAvailableActionException') !== false) { + $exception = new NoShardAvailableException($exception->getMessage(), $statusCode, $exception); + } else { + $exception = new ServerErrorResponseException($responseBody, $statusCode); + } + + $this->logRequestFail( + $request['http_method'], + $response['effective_url'], + $request['body'], + $request['headers'], + $response['status'], + $response['body'], + $response['transfer_stats']['total_time'], + $exception + ); + + throw $exception; + } + + private function tryDeserialize400Error($response) + { + return $this->tryDeserializeError($response, 'Elasticsearch\Common\Exceptions\BadRequest400Exception'); + } + + private function tryDeserialize500Error($response) + { + return $this->tryDeserializeError($response, 'Elasticsearch\Common\Exceptions\ServerErrorResponseException'); + } + + private function tryDeserializeError($response, $errorClass) + { + $error = $this->serializer->deserialize($response['body'], $response['transfer_stats']); + if (is_array($error) === true) { + // 2.0 structured exceptions + if (isset($error['error']['reason']) === true) { + + // Try to use root cause first (only grabs the first root cause) + $root = $error['error']['root_cause']; + if (isset($root) && isset($root[0])) { + $cause = $root[0]['reason']; + $type = $root[0]['type']; + } else { + $cause = $error['error']['reason']; + $type = $error['error']['type']; + } + + $original = new $errorClass($response['body'], $response['status']); + + return new $errorClass("$type: $cause", $response['status'], $original); + } elseif (isset($error['error']) === true) { + // <2.0 semi-structured exceptions + $original = new $errorClass($response['body'], $response['status']); + + return new $errorClass($error['error'], $response['status'], $original); + } + + // <2.0 "i just blew up" nonstructured exception + // $error is an array but we don't know the format, reuse the response body instead + return new $errorClass($response['body'], $response['status']); + } + + // <2.0 "i just blew up" nonstructured exception + return new $errorClass($response['body']); + } +} diff --git a/Framework/lib/Elasticsearch/Connections/ConnectionFactory.php b/Framework/lib/Elasticsearch/Connections/ConnectionFactory.php new file mode 100755 index 0000000..88d6dc9 --- /dev/null +++ b/Framework/lib/Elasticsearch/Connections/ConnectionFactory.php @@ -0,0 +1,67 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ConnectionFactory implements ConnectionFactoryInterface +{ + /** @var array */ + private $connectionParams; + + /** @var SerializerInterface */ + private $serializer; + + /** @var LoggerInterface */ + private $logger; + + /** @var LoggerInterface */ + private $tracer; + + /** @var callable */ + private $handler; + + /** + * Constructor + * + * @param callable $handler + * @param array $connectionParams + * @param SerializerInterface $serializer + * @param LoggerInterface $logger + * @param LoggerInterface $tracer + */ + public function __construct(callable $handler, array $connectionParams, SerializerInterface $serializer, LoggerInterface $logger, LoggerInterface $tracer) + { + $this->handler = $handler; + $this->connectionParams = $connectionParams; + $this->logger = $logger; + $this->tracer = $tracer; + $this->serializer = $serializer; + } + /** + * @param $hostDetails + * + * @return ConnectionInterface + */ + public function create($hostDetails) + { + return new Connection( + $this->handler, + $hostDetails, + $this->connectionParams, + $this->serializer, + $this->logger, + $this->tracer + ); + } +} diff --git a/Framework/lib/Elasticsearch/Connections/ConnectionFactoryInterface.php b/Framework/lib/Elasticsearch/Connections/ConnectionFactoryInterface.php new file mode 100755 index 0000000..242a321 --- /dev/null +++ b/Framework/lib/Elasticsearch/Connections/ConnectionFactoryInterface.php @@ -0,0 +1,35 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface ConnectionFactoryInterface +{ + /** + * @param $handler + * @param array $connectionParams + * @param SerializerInterface $serializer + * @param LoggerInterface $logger + * @param LoggerInterface $tracer + */ + public function __construct(callable $handler, array $connectionParams, + SerializerInterface $serializer, LoggerInterface $logger, LoggerInterface $tracer); + + /** + * @param $hostDetails + * + * @return ConnectionInterface + */ + public function create($hostDetails); +} diff --git a/Framework/lib/Elasticsearch/Connections/ConnectionInterface.php b/Framework/lib/Elasticsearch/Connections/ConnectionInterface.php new file mode 100755 index 0000000..44495db --- /dev/null +++ b/Framework/lib/Elasticsearch/Connections/ConnectionInterface.php @@ -0,0 +1,99 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface ConnectionInterface +{ + /** + * Constructor + * + * @param $handler + * @param array $hostDetails + * @param array $connectionParams connection-specific parameters + * @param \Elasticsearch\Serializers\SerializerInterface $serializer + * @param \Psr\Log\LoggerInterface $log Logger object + * @param \Psr\Log\LoggerInterface $trace Logger object + */ + public function __construct($handler, $hostDetails, $connectionParams, + SerializerInterface $serializer, LoggerInterface $log, LoggerInterface $trace); + + /** + * Get the transport schema for this connection + * + * @return string + */ + public function getTransportSchema(); + + /** + * Get the hostname for this connection + * + * @return string + */ + public function getHost(); + + /** + * Get the username:password string for this connection, null if not set + * + * @return null|string + */ + public function getUserPass(); + + /** + * Get the URL path suffix, null if not set + * + * @return null|string; + */ + public function getPath(); + + /** + * Check to see if this instance is marked as 'alive' + * + * @return bool + */ + public function isAlive(); + + /** + * Mark this instance as 'alive' + * + * @return void + */ + public function markAlive(); + + /** + * Mark this instance as 'dead' + * + * @return void + */ + public function markDead(); + + /** + * Return an associative array of information about the last request + * + * @return array + */ + public function getLastRequestInfo(); + + /** + * @param $method + * @param $uri + * @param null $params + * @param null $body + * @param array $options + * @param \Elasticsearch\Transport $transport + * @return mixed + */ + public function performRequest($method, $uri, $params = null, $body = null, $options = [], Transport $transport); +} diff --git a/Framework/lib/Elasticsearch/Endpoints/AbstractEndpoint.php b/Framework/lib/Elasticsearch/Endpoints/AbstractEndpoint.php new file mode 100755 index 0000000..0cfb68e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/AbstractEndpoint.php @@ -0,0 +1,286 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractEndpoint +{ + /** @var array */ + protected $params = array(); + + /** @var string */ + protected $index = null; + + /** @var string */ + protected $type = null; + + /** @var string|int */ + protected $id = null; + + /** @var string */ + protected $method = null; + + /** @var array */ + protected $body = null; + + /** @var array */ + private $options = []; + + /** @var SerializerInterface */ + protected $serializer; + + /** + * @return string[] + */ + abstract public function getParamWhitelist(); + + /** + * @return string + */ + abstract public function getURI(); + + /** + * @return string + */ + abstract public function getMethod(); + + + /** + * Set the parameters for this endpoint + * + * @param string[] $params Array of parameters + * @return $this + */ + public function setParams($params) + { + if (is_object($params) === true) { + $params = (array) $params; + } + + $this->checkUserParams($params); + $params = $this->convertCustom($params); + $this->extractOptions($params); + $this->params = $this->convertArraysToStrings($params); + + return $this; + } + + /** + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param string $index + * + * @return $this + */ + public function setIndex($index) + { + if ($index === null) { + return $this; + } + + if (is_array($index) === true) { + $index = array_map('trim', $index); + $index = implode(",", $index); + } + + $this->index = urlencode($index); + + return $this; + } + + /** + * @param string $type + * + * @return $this + */ + public function setType($type) + { + if ($type === null) { + return $this; + } + + if (is_array($type) === true) { + $type = array_map('trim', $type); + $type = implode(",", $type); + } + + $this->type = urlencode($type); + + return $this; + } + + /** + * @param int|string $docID + * + * @return $this + */ + public function setID($docID) + { + if ($docID === null) { + return $this; + } + + $this->id = urlencode($docID); + + return $this; + } + + /** + * @return array + */ + public function getBody() + { + return $this->body; + } + + /** + * @param string $endpoint + * + * @return string + */ + protected function getOptionalURI($endpoint) + { + $uri = array(); + $uri[] = $this->getOptionalIndex(); + $uri[] = $this->getOptionalType(); + $uri[] = $endpoint; + $uri = array_filter($uri); + + return '/' . implode('/', $uri); + } + + /** + * @return string + */ + private function getOptionalIndex() + { + if (isset($this->index) === true) { + return $this->index; + } else { + return '_all'; + } + } + + /** + * @return string + */ + private function getOptionalType() + { + if (isset($this->type) === true) { + return $this->type; + } else { + return ''; + } + } + + /** + * @param array $params + * + * @throws \Elasticsearch\Common\Exceptions\UnexpectedValueException + */ + private function checkUserParams($params) + { + if (isset($params) !== true) { + return; //no params, just return. + } + + $whitelist = array_merge($this->getParamWhitelist(), array('client', 'custom', 'filter_path')); + + foreach ($params as $key => $value) { + if (array_search($key, $whitelist) === false) { + throw new UnexpectedValueException(sprintf( + '"%s" is not a valid parameter. Allowed parameters are: "%s"', + $key, + implode('", "', $whitelist) + )); + } + } + } + + /** + * @param $params Note: this is passed by-reference! + */ + private function extractOptions(&$params) + { + // Extract out client options, then start transforming + if (isset($params['client']) === true) { + $this->options['client'] = $params['client']; + unset($params['client']); + } + + $ignore = isset($this->options['client']['ignore']) ? $this->options['client']['ignore'] : null; + if (isset($ignore) === true) { + if (is_string($ignore)) { + $this->options['client']['ignore'] = explode(",", $ignore); + } elseif (is_array($ignore)) { + $this->options['client']['ignore'] = $ignore; + } else { + $this->options['client']['ignore'] = [$ignore]; + } + } + } + + private function convertCustom($params) + { + if (isset($params['custom']) === true) { + foreach ($params['custom'] as $k => $v) { + $params[$k] = $v; + } + unset($params['custom']); + } + + return $params; + } + + private function convertArraysToStrings($params) + { + foreach ($params as $key => &$value) { + if (!($key === 'client' || $key == 'custom') && is_array($value) === true) { + if ($this->isNestedArray($value) !== true) { + $value = implode(",", $value); + } + } + } + + return $params; + } + + private function isNestedArray($a) + { + foreach ($a as $v) { + if (is_array($v)) { + return true; + } + } + + return false; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Bulk.php b/Framework/lib/Elasticsearch/Endpoints/Bulk.php new file mode 100755 index 0000000..3bc15bc --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Bulk.php @@ -0,0 +1,83 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Bulk extends AbstractEndpoint implements BulkEndpointInterface +{ + /** + * @param SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + /** + * @param string|array|\Traversable $body + * + * @return $this + */ + public function setBody($body) + { + if (empty($body)) { + return $this; + } + + if (is_array($body) === true || $body instanceof \Traversable) { + foreach ($body as $item) { + $this->body .= $this->serializer->serialize($item) . "\n"; + } + } else { + $this->body = $body; + } + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + return $this->getOptionalURI('_bulk'); + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'consistency', + 'refresh', + 'replication', + 'type', + 'fields', + 'pipeline', + '_source', + '_source_include', + '_source_exclude', + 'pipeline' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/BulkEndpointInterface.php b/Framework/lib/Elasticsearch/Endpoints/BulkEndpointInterface.php new file mode 100755 index 0000000..c7da254 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/BulkEndpointInterface.php @@ -0,0 +1,25 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface BulkEndpointInterface +{ + /** + * Constructor + * + * @param SerializerInterface $serializer A serializer + */ + public function __construct(SerializerInterface $serializer); +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Aliases.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Aliases.php new file mode 100755 index 0000000..959fbae --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Aliases.php @@ -0,0 +1,75 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Aliases extends AbstractEndpoint +{ + // A comma-separated list of alias names to return + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $name = $this->name; + $uri = "/_cat/aliases"; + + if (isset($name) === true) { + $uri = "/_cat/aliases/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 'format', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Allocation.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Allocation.php new file mode 100755 index 0000000..5322453 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Allocation.php @@ -0,0 +1,75 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Allocation extends AbstractEndpoint +{ + // A comma-separated list of node IDs or names to limit the returned information + private $node_id; + + /** + * @param $node_id + * + * @return $this + */ + public function setNodeId($node_id) + { + if (isset($node_id) !== true) { + return $this; + } + + $this->node_id = $node_id; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $node_id = $this->node_id; + $uri = "/_cat/allocation"; + + if (isset($node_id) === true) { + $uri = "/_cat/allocation/$node_id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'bytes', + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Count.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Count.php new file mode 100755 index 0000000..5ec9d64 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Count.php @@ -0,0 +1,55 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Count extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cat/count"; + + if (isset($index) === true) { + $uri = "/_cat/count/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Fielddata.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Fielddata.php new file mode 100755 index 0000000..eff659e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Fielddata.php @@ -0,0 +1,73 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Fielddata extends AbstractEndpoint +{ + private $fields; + + /** + * @param $fields + * + * @return $this + */ + public function setFields($fields) + { + if (isset($fields) !== true) { + return $this; + } + + $this->fields = $fields; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $fields = $this->fields; + $uri = "/_cat/fielddata"; + + if (isset($fields) === true) { + $uri = "/_cat/fielddata/$fields"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Health.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Health.php new file mode 100755 index 0000000..6a2cf97 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Health.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Health extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/health"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'ts', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Help.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Help.php new file mode 100755 index 0000000..d959ca8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Help.php @@ -0,0 +1,46 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Help extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'help', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Indices.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Indices.php new file mode 100755 index 0000000..97d8b17 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Indices.php @@ -0,0 +1,59 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Indices extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cat/indices"; + + if (isset($index) === true) { + $uri = "/_cat/indices/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'bytes', + 'local', + 'master_timeout', + 'h', + 'help', + 'pri', + 'v', + 'health', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Master.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Master.php new file mode 100755 index 0000000..e6dbfe1 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Master.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Master extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/master"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/NodeAttrs.php b/Framework/lib/Elasticsearch/Endpoints/Cat/NodeAttrs.php new file mode 100755 index 0000000..ff0d8fa --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/NodeAttrs.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class NodeAttrs extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/nodeattrs"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Nodes.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Nodes.php new file mode 100755 index 0000000..ffe497d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Nodes.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Nodes extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/nodes"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/PendingTasks.php b/Framework/lib/Elasticsearch/Endpoints/Cat/PendingTasks.php new file mode 100755 index 0000000..435f208 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/PendingTasks.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class PendingTasks extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/pending_tasks"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Plugins.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Plugins.php new file mode 100755 index 0000000..dbdc52a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Plugins.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Plugins extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/plugins"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Recovery.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Recovery.php new file mode 100755 index 0000000..4644e8f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Recovery.php @@ -0,0 +1,56 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Recovery extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cat/recovery"; + + if (isset($index) === true) { + $uri = "/_cat/recovery/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'bytes', + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Repositories.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Repositories.php new file mode 100755 index 0000000..1857cd5 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Repositories.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Repositories extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/repositories"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Segments.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Segments.php new file mode 100755 index 0000000..c7b728e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Segments.php @@ -0,0 +1,62 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Segments extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cat/segments"; + + if (isset($index) === true) { + $uri = "/_cat/segments/$index"; + } + + return $uri; + } + + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'h', + 'help', + 'v', + 's' + ); + } + + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Shards.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Shards.php new file mode 100755 index 0000000..1107037 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Shards.php @@ -0,0 +1,56 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Shards extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cat/shards"; + + if (isset($index) === true) { + $uri = "/_cat/shards/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'bytes', + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Snapshots.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Snapshots.php new file mode 100755 index 0000000..72d544f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Snapshots.php @@ -0,0 +1,72 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Snapshots extends AbstractEndpoint +{ + private $repository; + + /** + * @param $fields + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $repository = $this->repository; + if (isset($this->repository) === true) { + return "/_cat/snapshots/$repository/"; + } + + return "/_cat/snapshots/"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/Tasks.php b/Framework/lib/Elasticsearch/Endpoints/Cat/Tasks.php new file mode 100755 index 0000000..92cc033 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/Tasks.php @@ -0,0 +1,52 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Tasks extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + return "/_cat/tasks"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'format', + 'node_id', + 'actions', + 'detailed', + 'parent_node', + 'parent_task', + 'h', + 'help', + 'v', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cat/ThreadPool.php b/Framework/lib/Elasticsearch/Endpoints/Cat/ThreadPool.php new file mode 100755 index 0000000..d2a6e04 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cat/ThreadPool.php @@ -0,0 +1,54 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class ThreadPool extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cat/thread_pool"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'h', + 'help', + 'v', + 'full_id', + 'size', + 'thread_pool_patterns', + 's' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/ClearScroll.php b/Framework/lib/Elasticsearch/Endpoints/ClearScroll.php new file mode 100755 index 0000000..e960017 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/ClearScroll.php @@ -0,0 +1,74 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ClearScroll extends AbstractEndpoint +{ + // A comma-separated list of scroll IDs to clear + private $scroll_id; + + /** + * @param $scroll_id + * + * @return $this + */ + public function setScroll_Id($scroll_id) + { + if (isset($scroll_id) !== true) { + return $this; + } + + $this->scroll_id = $scroll_id; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->scroll_id) !== true) { + throw new Exceptions\RuntimeException( + 'scroll_id is required for Clearscroll' + ); + } + $scroll_id = $this->scroll_id; + $uri = "/_search/scroll/$scroll_id"; + + if (isset($scroll_id) === true) { + $uri = "/_search/scroll/$scroll_id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/AllocationExplain.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/AllocationExplain.php new file mode 100755 index 0000000..25014bf --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/AllocationExplain.php @@ -0,0 +1,62 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class AllocationExplain extends AbstractEndpoint +{ + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + return "/_cluster/allocation/explain"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'include_yes_decisions', + 'include_disk_info', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Health.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Health.php new file mode 100755 index 0000000..10e8a7c --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Health.php @@ -0,0 +1,59 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Health extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cluster/health"; + + if (isset($index) === true) { + $uri = "/_cluster/health/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'level', + 'local', + 'master_timeout', + 'timeout', + 'wait_for_active_shards', + 'wait_for_nodes', + 'wait_for_relocating_shards', + 'wait_for_status', + 'wait_for_events', + 'wait_for_no_relocating_shards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/AbstractNodesEndpoint.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/AbstractNodesEndpoint.php new file mode 100755 index 0000000..3b817b1 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/AbstractNodesEndpoint.php @@ -0,0 +1,47 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractNodesEndpoint extends AbstractEndpoint +{ + /** @var string A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes */ + protected $nodeID; + + /** + * @param $nodeID + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * + * @return $this + */ + public function setNodeID($nodeID) + { + if (isset($nodeID) !== true) { + return $this; + } + + if (!(is_array($nodeID) === true || is_string($nodeID) === true)) { + throw new InvalidArgumentException("invalid node_id"); + } + + if (is_array($nodeID) === true) { + $nodeID = implode(',', $nodeID); + } + + $this->nodeID = $nodeID; + + return $this; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/HotThreads.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/HotThreads.php new file mode 100755 index 0000000..eeb4a96 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/HotThreads.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class HotThreads extends AbstractNodesEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $node_id = $this->nodeID; + $uri = "/_cluster/nodes/hotthreads"; + + if (isset($node_id) === true) { + $uri = "/_cluster/nodes/$node_id/hotthreads"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'interval', + 'snapshots', + 'threads', + 'type', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Info.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Info.php new file mode 100755 index 0000000..bde5305 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Info.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Info extends AbstractNodesEndpoint +{ + // A comma-separated list of metrics you wish returned. Leave empty to return all. + private $metric; + + /** + * @param $metric + * + * @return $this + */ + public function setMetric($metric) + { + if (isset($metric) !== true) { + return $this; + } + + if (is_array($metric) === true) { + $metric = implode(",", $metric); + } + + $this->metric = $metric; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $node_id = $this->nodeID; + $metric = $this->metric; + $uri = "/_nodes"; + + if (isset($node_id) === true && isset($metric) === true) { + $uri = "/_nodes/$node_id/$metric"; + } elseif (isset($metric) === true) { + $uri = "/_nodes/$metric"; + } elseif (isset($node_id) === true) { + $uri = "/_nodes/$node_id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flat_settings', + 'human', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Shutdown.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Shutdown.php new file mode 100755 index 0000000..6b7a6f2 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Shutdown.php @@ -0,0 +1,49 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Shutdown extends AbstractNodesEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $node_id = $this->nodeID; + $uri = "/_shutdown"; + + if (isset($node_id) === true) { + $uri = "/_cluster/nodes/$node_id/_shutdown"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'delay', + 'exit', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Stats.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Stats.php new file mode 100755 index 0000000..90da06c --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Nodes/Stats.php @@ -0,0 +1,110 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Stats extends AbstractNodesEndpoint +{ + // Limit the information returned to the specified metrics + private $metric; + + // Limit the information returned for `indices` metric to the specific index metrics. Isn't used if `indices` (or `all`) metric isn't specified. + private $indexMetric; + + /** + * @param $metric + * + * @return $this + */ + public function setMetric($metric) + { + if (isset($metric) !== true) { + return $this; + } + + if (is_array($metric) === true) { + $metric = implode(",", $metric); + } + + $this->metric = $metric; + + return $this; + } + + /** + * @param $indexMetric + * + * @return $this + */ + public function setIndexMetric($indexMetric) + { + if (isset($indexMetric) !== true) { + return $this; + } + + if (is_array($indexMetric) === true) { + $indexMetric = implode(",", $indexMetric); + } + + $this->indexMetric = $indexMetric; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $metric = $this->metric; + $index_metric = $this->indexMetric; + $node_id = $this->nodeID; + $uri = "/_nodes/stats"; + + if (isset($node_id) === true && isset($metric) === true && isset($index_metric) === true) { + $uri = "/_nodes/$node_id/stats/$metric/$index_metric"; + } elseif (isset($metric) === true && isset($index_metric) === true) { + $uri = "/_nodes/stats/$metric/$index_metric"; + } elseif (isset($node_id) === true && isset($metric) === true) { + $uri = "/_nodes/$node_id/stats/$metric"; + } elseif (isset($metric) === true) { + $uri = "/_nodes/stats/$metric"; + } elseif (isset($node_id) === true) { + $uri = "/_nodes/$node_id/stats"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'completion_fields', + 'fielddata_fields', + 'fields', + 'groups', + 'human', + 'level', + 'types', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/PendingTasks.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/PendingTasks.php new file mode 100755 index 0000000..3ceac3a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/PendingTasks.php @@ -0,0 +1,46 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class PendingTasks extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cluster/pending_tasks"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Reroute.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Reroute.php new file mode 100755 index 0000000..4113053 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Reroute.php @@ -0,0 +1,68 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Reroute extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $uri = "/_cluster/reroute"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'dry_run', + 'filter_metadata', + 'master_timeout', + 'timeout', + 'explain', + 'metric' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Get.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Get.php new file mode 100755 index 0000000..12a5349 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Get.php @@ -0,0 +1,48 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Get extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/_cluster/settings"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flat_settings', + 'master_timeout', + 'timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Put.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Put.php new file mode 100755 index 0000000..522e7b1 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Settings/Put.php @@ -0,0 +1,63 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $uri = "/_cluster/settings"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flat_settings', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/State.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/State.php new file mode 100755 index 0000000..94af3b1 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/State.php @@ -0,0 +1,82 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class State extends AbstractEndpoint +{ + // Limit the information returned to the specified metrics + private $metric; + + /** + * @param $metric + * + * @return $this + */ + public function setMetric($metric) + { + if (isset($metric) !== true) { + return $this; + } + + if (is_array($metric) === true) { + $metric = implode(",", $metric); + } + + $this->metric = $metric; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $metric = $this->metric; + $uri = "/_cluster/state"; + + if (isset($metric) === true && isset($index) === true) { + $uri = "/_cluster/state/$metric/$index"; + } elseif (isset($metric) === true) { + $uri = "/_cluster/state/$metric"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout', + 'flat_settings', + 'index_templates', + 'expand_wildcards', + 'ignore_unavailable', + 'allow_no_indices' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Cluster/Stats.php b/Framework/lib/Elasticsearch/Endpoints/Cluster/Stats.php new file mode 100755 index 0000000..729b611 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Cluster/Stats.php @@ -0,0 +1,70 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Stats extends AbstractEndpoint +{ + // A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + private $nodeID; + + /** + * @param $node_id + * + * @return $this + */ + public function setNodeID($node_id) + { + if (isset($node_id) !== true) { + return $this; + } + + $this->nodeID = $node_id; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $node_id = $this->nodeID; + $uri = "/_cluster/stats"; + + if (isset($node_id) === true) { + $uri = "/_cluster/stats/nodes/$node_id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flat_settings', + 'human', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Count.php b/Framework/lib/Elasticsearch/Endpoints/Count.php new file mode 100755 index 0000000..7bedb4b --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Count.php @@ -0,0 +1,86 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Count extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_count"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_count"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_count"; + } elseif (isset($index) === true) { + $uri = "/$index/_count"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'min_score', + 'preference', + 'routing', + 'source', + 'q', + 'df', + 'default_operator', + 'analyzer', + 'lowercase_expanded_terms', + 'analyze_wildcard', + 'lenient', + 'lowercase_expanded_terms' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/CountPercolate.php b/Framework/lib/Elasticsearch/Endpoints/CountPercolate.php new file mode 100755 index 0000000..f87d6b5 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/CountPercolate.php @@ -0,0 +1,90 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class CountPercolate extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for CountPercolate' + ); + } + + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for CountPercolate' + ); + } + + $index = $this->index; + $type = $this->type; + $id = $this->id; + $uri = "/$index/$type/_percolate/count"; + + if (isset($id) === true) { + $uri = "/$index/$type/$id/_percolate/count"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'routing', + 'preference', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'percolate_index', + 'percolate_type', + 'version', + 'version_type' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Create.php b/Framework/lib/Elasticsearch/Endpoints/Create.php new file mode 100755 index 0000000..bbecabe --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Create.php @@ -0,0 +1,107 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Create extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Create' + ); + } + + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Create' + ); + } + + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Create' + ); + } + + $id = $this->id; + $index = $this->index; + $type = $this->type; + return "/$index/$type/$id/_create"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'consistency', + 'op_type', + 'parent', + 'percolate', + 'refresh', + 'replication', + 'routing', + 'timeout', + 'timestamp', + 'ttl', + 'version', + 'version_type', + 'pipeline' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Document body must be set for create request'); + } else { + return $this->body; + } + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Delete.php new file mode 100755 index 0000000..3f51bae --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Delete.php @@ -0,0 +1,75 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Delete' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Delete' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Delete' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'consistency', + 'parent', + 'refresh', + 'replication', + 'routing', + 'timeout', + 'version', + 'version_type', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Exists.php b/Framework/lib/Elasticsearch/Endpoints/Exists.php new file mode 100755 index 0000000..8ab0bed --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Exists.php @@ -0,0 +1,72 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Exists extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Exists' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Exists' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Exists' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'parent', + 'preference', + 'realtime', + 'refresh', + 'routing', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Explain.php b/Framework/lib/Elasticsearch/Endpoints/Explain.php new file mode 100755 index 0000000..7d1bb2e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Explain.php @@ -0,0 +1,99 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Explain extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Explain' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Explain' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Explain' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id/_explain"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id/_explain"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'analyze_wildcard', + 'analyzer', + 'default_operator', + 'df', + 'fields', + 'lenient', + 'lowercase_expanded_terms', + 'parent', + 'preference', + 'q', + 'routing', + 'source', + '_source', + '_source_exclude', + '_source_include', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/FieldStats.php b/Framework/lib/Elasticsearch/Endpoints/FieldStats.php new file mode 100755 index 0000000..1ca80d8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/FieldStats.php @@ -0,0 +1,73 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class FieldStats extends AbstractEndpoint +{ + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_field_stats"; + + if (isset($index) === true) { + $uri = "/$index/_field_stats"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'fields', + 'level', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'fields' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Get.php b/Framework/lib/Elasticsearch/Endpoints/Get.php new file mode 100755 index 0000000..34fa666 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Get.php @@ -0,0 +1,113 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** @var bool */ + private $returnOnlySource = false; + + /** @var bool */ + private $checkOnlyExistance = false; + + /** + * @return $this + */ + public function returnOnlySource() + { + $this->returnOnlySource = true; + + return $this; + } + + /** + * @return $this + */ + public function checkOnlyExistance() + { + $this->checkOnlyExistance = true; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Get' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Get' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Get' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id"; + } + + if ($this->returnOnlySource === true) { + $uri .= '/_source'; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'fields', + 'parent', + 'preference', + 'realtime', + 'refresh', + 'routing', + '_source', + '_source_exclude', + '_source_include', + 'version', + 'version_type', + 'stored_fields' + ); + } + + /** + * @return string + */ + public function getMethod() + { + if ($this->checkOnlyExistance === true) { + return 'HEAD'; + } else { + return 'GET'; + } + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Index.php b/Framework/lib/Elasticsearch/Endpoints/Index.php new file mode 100755 index 0000000..ba3c82a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Index.php @@ -0,0 +1,123 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Index extends AbstractEndpoint +{ + /** @var bool */ + private $createIfAbsent = false; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return $this + */ + public function createIfAbsent() + { + $this->createIfAbsent = true; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Index' + ); + } + + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Index' + ); + } + + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type"; + + if (isset($id) === true) { + $uri = "/$index/$type/$id"; + } + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'consistency', + 'op_type', + 'parent', + 'percolate', + 'refresh', + 'replication', + 'routing', + 'timeout', + 'timestamp', + 'ttl', + 'version', + 'version_type', + 'pipeline' + ); + } + + /** + * @return string + */ + public function getMethod() + { + if (isset($this->id) === true) { + return 'PUT'; + } else { + return 'POST'; + } + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Document body must be set for index request'); + } else { + return $this->body; + } + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/AbstractAliasEndpoint.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/AbstractAliasEndpoint.php new file mode 100755 index 0000000..c3ebf84 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/AbstractAliasEndpoint.php @@ -0,0 +1,38 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractAliasEndpoint extends AbstractEndpoint +{ + /** @var null|string */ + protected $name = null; + + /** + * @param $name + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * + * @return $this + */ + public function setName($name) + { + if (is_string($name) !== true) { + throw new InvalidArgumentException('Name must be a string'); + } + $this->name = urlencode($name); + + return $this; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Delete.php new file mode 100755 index 0000000..2ed4681 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Delete.php @@ -0,0 +1,83 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + // A comma-separated list of aliases to delete (supports wildcards); use `_all` to delete all aliases for the specified indices. + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Delete' + ); + } + if (isset($this->name) !== true) { + throw new Exceptions\RuntimeException( + 'name is required for Delete' + ); + } + $index = $this->index; + $name = $this->name; + $uri = "/$index/_alias/$name"; + + if (isset($index) === true && isset($name) === true) { + $uri = "/$index/_alias/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Exists.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Exists.php new file mode 100755 index 0000000..abc978a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Exists.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Exists extends AbstractEndpoint +{ + // A comma-separated list of alias names to return + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $name = $this->name; + $uri = "/_alias/$name"; + + if (isset($index) === true && isset($name) === true) { + $uri = "/$index/_alias/$name"; + } elseif (isset($index) === true) { + $uri = "/$index/_alias"; + } elseif (isset($name) === true) { + $uri = "/_alias/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Get.php new file mode 100755 index 0000000..8b2ae03 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Get.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // A comma-separated list of alias names to return + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $name = $this->name; + $uri = "/_alias"; + + if (isset($index) === true && isset($name) === true) { + $uri = "/$index/_alias/$name"; + } elseif (isset($index) === true) { + $uri = "/$index/_alias"; + } elseif (isset($name) === true) { + $uri = "/_alias/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Put.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Put.php new file mode 100755 index 0000000..a91d625 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Alias/Put.php @@ -0,0 +1,97 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + // The name of the alias to be created or updated + private $name; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->name) !== true) { + throw new Exceptions\RuntimeException( + 'name is required for Put' + ); + } + + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Put' + ); + } + $index = $this->index; + $name = $this->name; + $uri = "/$index/_alias/$name"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Get.php new file mode 100755 index 0000000..4e9287f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Get.php @@ -0,0 +1,75 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // A comma-separated list of alias names to filter + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $name = $this->name; + $uri = "/_aliases"; + + if (isset($index) === true && isset($name) === true) { + $uri = "/$index/_aliases/$name"; + } elseif (isset($name) === true) { + $uri = "/_aliases/$name"; + } elseif (isset($index) === true) { + $uri = "/$index/_aliases"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Update.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Update.php new file mode 100755 index 0000000..a715b0f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Aliases/Update.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Update extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $uri = "/_aliases"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Update Aliases'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Analyze.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Analyze.php new file mode 100755 index 0000000..ac77879 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Analyze.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Analyze extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_analyze"; + + if (isset($index) === true) { + $uri = "/$index/_analyze"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'analyzer', + 'field', + 'filter', + 'index', + 'prefer_local', + 'text', + 'tokenizer', + 'format', + 'char_filter', + 'explain', + 'attributes', + 'format' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Cache/Clear.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Cache/Clear.php new file mode 100755 index 0000000..787072d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Cache/Clear.php @@ -0,0 +1,62 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Clear extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cache/clear"; + + if (isset($index) === true) { + $uri = "/$index/_cache/clear"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'field_data', + 'fielddata', + 'fields', + 'filter', + 'filter_cache', + 'filter_keys', + 'id', + 'id_cache', + 'index', + 'recycler', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/ClearCache.php b/Framework/lib/Elasticsearch/Endpoints/Indices/ClearCache.php new file mode 100755 index 0000000..4b502ba --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/ClearCache.php @@ -0,0 +1,62 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ClearCache extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_cache/clear"; + + if (isset($index) === true) { + $uri = "/$index/_cache/clear"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'field_data', + 'fielddata', + 'fields', + 'filter', + 'filter_cache', + 'filter_keys', + 'id', + 'id_cache', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'index', + 'recycler', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Close.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Close.php new file mode 100755 index 0000000..3d5bf7e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Close.php @@ -0,0 +1,61 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Close extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Close' + ); + } + $index = $this->index; + $uri = "/$index/_close"; + + if (isset($index) === true) { + $uri = "/$index/_close"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Create.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Create.php new file mode 100755 index 0000000..505d252 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Create.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Create extends AbstractEndpoint +{ + /** + * @param array|object $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Create' + ); + } + $index = $this->index; + $uri = "/$index"; + + if (isset($index) === true) { + $uri = "/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + 'update_all_types', + 'wait_for_active_shards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Delete.php new file mode 100755 index 0000000..b832e71 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Delete.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/$index"; + + if (isset($index) === true) { + $uri = "/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Exists.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Exists.php new file mode 100755 index 0000000..ac45e3f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Exists.php @@ -0,0 +1,60 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Exists extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Exists' + ); + } + $index = $this->index; + $uri = "/$index"; + + if (isset($index) === true) { + $uri = "/$index"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Exists/Types.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Exists/Types.php new file mode 100755 index 0000000..a5b4b67 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Exists/Types.php @@ -0,0 +1,63 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Types extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Types Exists' + ); + } + + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Types Exists' + ); + } + + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Field/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Field/Get.php new file mode 100755 index 0000000..c991234 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Field/Get.php @@ -0,0 +1,88 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // A comma-separated list of fields + private $field; + + /** + * @param $field + * + * @return $this + */ + public function setField($field) + { + if (isset($field) !== true) { + return $this; + } + + $this->field = $field; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->field) !== true) { + throw new Exceptions\RuntimeException( + 'field is required for Get' + ); + } + $index = $this->index; + $type = $this->type; + $field = $this->field; + $uri = "/_mapping/field/$field"; + + if (isset($index) === true && isset($type) === true && isset($field) === true) { + $uri = "/$index/_mapping/$type/field/$field"; + } elseif (isset($type) === true && isset($field) === true) { + $uri = "/_mapping/$type/field/$field"; + } elseif (isset($index) === true && isset($field) === true) { + $uri = "/$index/_mapping/field/$field"; + } elseif (isset($field) === true) { + $uri = "/_mapping/field/$field"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'include_defaults', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Flush.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Flush.php new file mode 100755 index 0000000..f8d7c4b --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Flush.php @@ -0,0 +1,66 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Flush extends AbstractEndpoint +{ + protected $synced = false; + + public function setSynced($synced) + { + $this->synced = $synced; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_flush"; + + if (isset($index) === true) { + $uri = "/$index/_flush"; + } + + if ($this->synced === true) { + $uri .= "/synced"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'force', + 'full', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'wait_if_ongoing' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/ForceMerge.php b/Framework/lib/Elasticsearch/Endpoints/Indices/ForceMerge.php new file mode 100755 index 0000000..3e6e0b4 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/ForceMerge.php @@ -0,0 +1,57 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ForceMerge extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_forcemerge"; + + if (isset($index) === true) { + $uri = "/$index/_forcemerge"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flush', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'max_num_segments', + 'only_expunge_deletes', + 'operation_threading', + 'wait_for_merge', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Gateway/Snapshot.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Gateway/Snapshot.php new file mode 100755 index 0000000..b492cea --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Gateway/Snapshot.php @@ -0,0 +1,52 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Snapshot extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_gateway/snapshot"; + + if (isset($index) === true) { + $uri = "/$index/_gateway/snapshot"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Get.php new file mode 100755 index 0000000..58a7de7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Get.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + private $feature; + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Get' + ); + } + $index = $this->index; + $feature = $this->feature; + $uri = "/$index"; + + if (isset($feature) === true) { + $uri = "/$index/$feature"; + } + + return $uri; + } + + public function setFeature($feature) + { + if (isset($feature) !== true) { + return $this; + } + + if (is_array($feature) === true) { + $feature = implode(",", $feature); + } + + $this->feature = $feature; + + return $this; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'human' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Delete.php new file mode 100755 index 0000000..87ac13d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Delete.php @@ -0,0 +1,63 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Delete' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Delete' + ); + } + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/_mapping"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_mapping"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Get.php new file mode 100755 index 0000000..88568e6 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Get.php @@ -0,0 +1,59 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_mapping"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/_mapping/$type"; + } elseif (isset($type) === true) { + $uri = "/_mapping/$type"; + } elseif (isset($index) === true) { + $uri = "/$index/_mapping"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'wildcard_expansion', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/GetField.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/GetField.php new file mode 100755 index 0000000..068be33 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/GetField.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class GetField extends AbstractEndpoint +{ + /** @var string */ + private $fields; + + /** + * @param string|array $fields + * + * @return $this + */ + public function setFields($fields) + { + if (isset($fields) !== true) { + return $this; + } + + if (is_array($fields) === true) { + $fields = implode(",", $fields); + } + + $this->fields = $fields; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->fields) !== true) { + throw new Exceptions\RuntimeException( + 'fields is required for Get Field Mapping' + ); + } + $uri = $this->getOptionalURI('_mapping/field'); + + return $uri.'/'.$this->fields; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'include_defaults', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Put.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Put.php new file mode 100755 index 0000000..1639ca7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Mapping/Put.php @@ -0,0 +1,96 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Put' + ); + } + $index = $this->index; + $type = $this->type; + $uri = "/_mapping/$type"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_mapping"; + } elseif (isset($type) === true) { + $uri = "/_mapping/$type"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_conflicts', + 'timeout', + 'master_timeout', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'update_all_types' + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Put Mapping'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Open.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Open.php new file mode 100755 index 0000000..6808659 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Open.php @@ -0,0 +1,61 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Open extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Open' + ); + } + $index = $this->index; + $uri = "/$index/_open"; + + if (isset($index) === true) { + $uri = "/$index/_open"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Recovery.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Recovery.php new file mode 100755 index 0000000..11e11cf --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Recovery.php @@ -0,0 +1,52 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Recovery extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_recovery"; + + if (isset($index) === true) { + $uri = "/$index/_recovery"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'detailed', + 'active_only', + 'human' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Refresh.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Refresh.php new file mode 100755 index 0000000..e7938d1 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Refresh.php @@ -0,0 +1,54 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Refresh extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_refresh"; + + if (isset($index) === true) { + $uri = "/$index/_refresh"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'force', + 'operation_threading', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Rollover.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Rollover.php new file mode 100755 index 0000000..bc188ea --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Rollover.php @@ -0,0 +1,109 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Rollover extends AbstractEndpoint +{ + private $alias; + private $newIndex; + + /** + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) + { + if ($alias === null) { + return $this; + } + + $this->alias = urlencode($alias); + return $this; + } + + /** + * @param string $newIndex + * + * @return $this + */ + public function setNewIndex($newIndex) + { + if ($newIndex === null) { + return $this; + } + + $this->newIndex = urlencode($newIndex); + return $this; + } + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->alias) !== true) { + throw new Exceptions\RuntimeException( + 'alias name is required for Rollover' + ); + } + + $uri = "/{$this->alias}/_rollover"; + + if (isset($this->newIndex) === true) { + $uri .= "/{$this->newIndex}"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + 'wait_for_active_shards', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Seal.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Seal.php new file mode 100755 index 0000000..c6f5138 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Seal.php @@ -0,0 +1,50 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Seal extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_seal"; + + if (isset($index) === true) { + $uri = "/$index/_seal"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array(); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Segments.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Segments.php new file mode 100755 index 0000000..8ade291 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Segments.php @@ -0,0 +1,54 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Segments extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_segments"; + + if (isset($index) === true) { + $uri = "/$index/_segments"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'human', + 'operation_threading', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Get.php new file mode 100755 index 0000000..943bfd9 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Get.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // The name of the settings that should be included + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $name = $this->name; + $uri = "/_settings"; + + if (isset($index) === true && isset($name) === true) { + $uri = "/$index/_settings/$name"; + } elseif (isset($name) === true) { + $uri = "/_settings/$name"; + } elseif (isset($index) === true) { + $uri = "/$index/_settings"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'flat_settings', + 'local', + 'include_defaults' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Put.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Put.php new file mode 100755 index 0000000..57f211a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Settings/Put.php @@ -0,0 +1,86 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_settings"; + + if (isset($index) === true) { + $uri = "/$index/_settings"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'flat_settings', + 'preserve_existing' + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Put Settings'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/ShardStores.php b/Framework/lib/Elasticsearch/Endpoints/Indices/ShardStores.php new file mode 100755 index 0000000..10dc447 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/ShardStores.php @@ -0,0 +1,59 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class ShardStores extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_shard_stores"; + + if (isset($index) === true) { + $uri = "/$index/_shard_stores"; + } + + return $uri; + } + + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'status', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'operation_threading' + ); + } + + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Shrink.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Shrink.php new file mode 100755 index 0000000..b4e7832 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Shrink.php @@ -0,0 +1,101 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * + * @link http://elastic.co + */ +class Shrink extends AbstractEndpoint +{ + // The name of the target index to shrink into + private $target; + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $target + * + * @return $this + */ + public function setTarget($target) + { + if (isset($target) !== true) { + return $this; + } + $this->target = $target; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\BadMethodCallException + * + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Shrink' + ); + } + if (isset($this->target) !== true) { + throw new Exceptions\RuntimeException( + 'target is required for Shrink' + ); + } + $index = $this->index; + $target = $this->target; + $uri = "/$index/_shrink/$target"; + if (isset($index) === true && isset($target) === true) { + $uri = "/$index/_shrink/$target"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + //TODO Fix Me! + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Snapshotindex.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Snapshotindex.php new file mode 100755 index 0000000..e30530b --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Snapshotindex.php @@ -0,0 +1,52 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Snapshotindex extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_gateway/snapshot"; + + if (isset($index) === true) { + $uri = "/$index/_gateway/snapshot"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Stats.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Stats.php new file mode 100755 index 0000000..899afc6 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Stats.php @@ -0,0 +1,85 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Stats extends AbstractEndpoint +{ + // Limit the information returned the specific metrics. + private $metric; + + /** + * @param $metric + * + * @return $this + */ + public function setMetric($metric) + { + if (isset($metric) !== true) { + return $this; + } + + if (is_array($metric)) { + $metric = implode(",", $metric); + } + + $this->metric = $metric; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $metric = $this->metric; + $uri = "/_stats"; + + if (isset($index) === true && isset($metric) === true) { + $uri = "/$index/_stats/$metric"; + } elseif (isset($index) === true) { + $uri = "/$index/_stats"; + } elseif (isset($metric) === true) { + $uri = "/_stats/$metric"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'completion_fields', + 'fielddata_fields', + 'fields', + 'groups', + 'human', + 'level', + 'types', + 'metric' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Status.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Status.php new file mode 100755 index 0000000..fc52f84 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Status.php @@ -0,0 +1,56 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Status extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_status"; + + if (isset($index) === true) { + $uri = "/$index/_status"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'human', + 'operation_threading', + 'recovery', + 'snapshot', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Template/AbstractTemplateEndpoint.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/AbstractTemplateEndpoint.php new file mode 100755 index 0000000..cde0225 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/AbstractTemplateEndpoint.php @@ -0,0 +1,32 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractTemplateEndpoint extends AbstractEndpoint +{ + /** @var string */ + protected $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Delete.php new file mode 100755 index 0000000..044dce6 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Delete.php @@ -0,0 +1,78 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Delete extends AbstractEndpoint +{ + // The name of the template + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->name) !== true) { + throw new Exceptions\RuntimeException( + 'name is required for Delete' + ); + } + $name = $this->name; + $uri = "/_template/$name"; + + if (isset($name) === true) { + $uri = "/_template/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'timeout', + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Exists.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Exists.php new file mode 100755 index 0000000..ebf6fdf --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Exists.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Exists extends AbstractEndpoint +{ + // The name of the template + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->name) !== true) { + throw new Exceptions\RuntimeException( + 'name is required for Exists' + ); + } + $name = $this->name; + $uri = "/_template/$name"; + + if (isset($name) === true) { + $uri = "/_template/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'local', + 'master_timeout' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Get.php new file mode 100755 index 0000000..7747206 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Get.php @@ -0,0 +1,73 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // The name of the template + private $name; + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + $name = $this->name; + $uri = "/_template"; + + if (isset($name) === true) { + $uri = "/_template/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'flat_settings', + 'local', + 'master_timeout' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Put.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Put.php new file mode 100755 index 0000000..3aca046 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Template/Put.php @@ -0,0 +1,110 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + // The name of the template + private $name; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + if (isset($name) !== true) { + return $this; + } + + $this->name = $name; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->name) !== true) { + throw new Exceptions\RuntimeException( + 'name is required for Put' + ); + } + $name = $this->name; + $uri = "/_template/$name"; + + if (isset($name) === true) { + $uri = "/_template/$name"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'order', + 'timeout', + 'master_timeout', + 'flat_settings', + 'create' + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Put Template'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Type/Exists.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Type/Exists.php new file mode 100755 index 0000000..b295189 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Type/Exists.php @@ -0,0 +1,60 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Exists extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Exists' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Exists' + ); + } + $uri = "/{$this->index}/_mapping/{$this->type}"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Get.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Get.php new file mode 100755 index 0000000..d9cb7be --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Get.php @@ -0,0 +1,65 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Get extends AbstractEndpoint +{ + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_upgrade"; + + if (isset($index) === true) { + $uri = "/$index/_upgrade"; + } + + + return $uri; + } + + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'wait_for_completion', + 'only_ancient_segments', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Post.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Post.php new file mode 100755 index 0000000..5b00f68 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Upgrade/Post.php @@ -0,0 +1,65 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class Post extends AbstractEndpoint +{ + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_upgrade"; + + if (isset($index) === true) { + $uri = "/$index/_upgrade"; + } + + + return $uri; + } + + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'wait_for_completion', + 'only_ancient_segments', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/Validate/Query.php b/Framework/lib/Elasticsearch/Endpoints/Indices/Validate/Query.php new file mode 100755 index 0000000..a963038 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/Validate/Query.php @@ -0,0 +1,71 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Query extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + return $this->getOptionalURI('_validate/query'); + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'explain', + 'ignore_indices', + 'operation_threading', + 'source', + 'q', + 'df', + 'default_operator', + 'analyzer', + 'analyze_wildcard', + 'lenient', + 'lowercase_expanded_terms' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Indices/ValidateQuery.php b/Framework/lib/Elasticsearch/Endpoints/Indices/ValidateQuery.php new file mode 100755 index 0000000..df94c02 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Indices/ValidateQuery.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ValidateQuery extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_validate/query"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_validate/query"; + } elseif (isset($index) === true) { + $uri = "/$index/_validate/query"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'explain', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'operation_threading', + 'source', + 'q', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Info.php b/Framework/lib/Elasticsearch/Endpoints/Info.php new file mode 100755 index 0000000..dc157d7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Info.php @@ -0,0 +1,42 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Info extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Delete.php new file mode 100755 index 0000000..b61e9e8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Delete.php @@ -0,0 +1,54 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for DeletePipeline' + ); + } + $id = $this->id; + $uri = "/_ingest/pipeline/$id"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'timeout' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Get.php b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Get.php new file mode 100755 index 0000000..d5cf38f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Get.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + return '/_ingest/pipeline/*'; + } + + $id = $this->id; + + return "/_ingest/pipeline/$id"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Put.php b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Put.php new file mode 100755 index 0000000..d8707b3 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Ingest/Pipeline/Put.php @@ -0,0 +1,71 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for PutPipeline' + ); + } + $id = $this->id; + $uri = "/_ingest/pipeline/$id"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'timeout' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Ingest/Simulate.php b/Framework/lib/Elasticsearch/Endpoints/Ingest/Simulate.php new file mode 100755 index 0000000..f4570bb --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Ingest/Simulate.php @@ -0,0 +1,65 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Simulate extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) === true) { + return "/_ingest/pipeline/{$this->id}/_simulate"; + } + return "/_ingest/pipeline/_simulate"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'verbose', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/MPercolate.php b/Framework/lib/Elasticsearch/Endpoints/MPercolate.php new file mode 100755 index 0000000..47d20a8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/MPercolate.php @@ -0,0 +1,78 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class MPercolate extends AbstractEndpoint implements BulkEndpointInterface +{ + /** + * @param SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + /** + * @param string|array $body + * + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + if (is_array($body) === true) { + $bulkBody = ""; + foreach ($body as $item) { + $bulkBody .= $this->serializer->serialize($item)."\n"; + } + $body = $bulkBody; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + return $this->getOptionalURI('_mpercolate'); + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/MTermVectors.php b/Framework/lib/Elasticsearch/Endpoints/MTermVectors.php new file mode 100755 index 0000000..e723920 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/MTermVectors.php @@ -0,0 +1,70 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class MTermVectors extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + return $this->getOptionalURI('_mtermvectors'); + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ids', + 'term_statistics', + 'field_statistics', + 'fields', + 'offsets', + 'positions', + 'payloads', + 'preference', + 'routing', + 'parent', + 'realtime' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Mget.php b/Framework/lib/Elasticsearch/Endpoints/Mget.php new file mode 100755 index 0000000..2d7dd56 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Mget.php @@ -0,0 +1,93 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Mget extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_mget"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_mget"; + } elseif (isset($index) === true) { + $uri = "/$index/_mget"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_mget"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'fields', + 'preference', + 'realtime', + 'refresh', + '_source', + '_source_exclude', + '_source_include', + 'routing', + 'stored_fields' + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for MGet'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Msearch.php b/Framework/lib/Elasticsearch/Endpoints/Msearch.php new file mode 100755 index 0000000..b4b4d1e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Msearch.php @@ -0,0 +1,103 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Msearch extends AbstractEndpoint +{ + /** + * @param SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + /** + * @param array|string $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + if (is_array($body) === true) { + $bulkBody = ""; + foreach ($body as $item) { + $bulkBody .= $this->serializer->serialize($item)."\n"; + } + $body = $bulkBody; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_msearch"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_msearch"; + } elseif (isset($index) === true) { + $uri = "/$index/_msearch"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_msearch"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'search_type', + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for MSearch'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Percolate.php b/Framework/lib/Elasticsearch/Endpoints/Percolate.php new file mode 100755 index 0000000..4418d7d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Percolate.php @@ -0,0 +1,98 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Percolate extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Percolate' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Percolate' + ); + } + $index = $this->index; + $type = $this->type; + $id = $this->id; + $uri = "/$index/$type/_percolate"; + + if (isset($id) === true) { + $uri = "/$index/$type/$id/_percolate"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'routing', + 'preference', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'percolate_index', + 'percolate_type', + 'version', + 'version_type', + 'percolate_format' + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Ping.php b/Framework/lib/Elasticsearch/Endpoints/Ping.php new file mode 100755 index 0000000..a11d902 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Ping.php @@ -0,0 +1,42 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Ping extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $uri = "/"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'HEAD'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Reindex.php b/Framework/lib/Elasticsearch/Endpoints/Reindex.php new file mode 100755 index 0000000..5f8b97c --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Reindex.php @@ -0,0 +1,63 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Reindex extends AbstractEndpoint +{ + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'refresh', + 'timeout', + 'consistency', + 'wait_for_completion', + 'requests_per_second', + ); + } + + /** + * @return string + */ + public function getURI() + { + return '/_reindex'; + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/RenderSearchTemplate.php b/Framework/lib/Elasticsearch/Endpoints/RenderSearchTemplate.php new file mode 100755 index 0000000..c31eb72 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/RenderSearchTemplate.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +class RenderSearchTemplate extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + $id = $this->id; + + $uri = "/_render/template"; + + if (isset($id) === true) { + $uri = "/_render/template/$id"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array(); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Script/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Script/Delete.php new file mode 100755 index 0000000..887c9f8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Script/Delete.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** @var String */ + private $lang; + + /** + * @param $lang + * + * @return $this + */ + public function setLang($lang) + { + if (isset($lang) !== true) { + return $this; + } + + $this->lang = $lang; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->lang) !== true) { + throw new Exceptions\RuntimeException( + 'lang is required for Put' + ); + } + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for put' + ); + } + $id = $this->id; + $lang = $this->lang; + $uri = "/_scripts/$lang/$id"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'version', + 'version_type' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Script/Get.php b/Framework/lib/Elasticsearch/Endpoints/Script/Get.php new file mode 100755 index 0000000..78c01c8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Script/Get.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** @var String */ + private $lang; + + /** + * @param $lang + * + * @return $this + */ + public function setLang($lang) + { + if (isset($lang) !== true) { + return $this; + } + + $this->lang = $lang; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->lang) !== true) { + throw new Exceptions\RuntimeException( + 'lang is required for Put' + ); + } + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for put' + ); + } + $id = $this->id; + $lang = $this->lang; + $uri = "/_scripts/$lang/$id"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'version_type', + 'version' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Script/Put.php b/Framework/lib/Elasticsearch/Endpoints/Script/Put.php new file mode 100755 index 0000000..d10603e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Script/Put.php @@ -0,0 +1,96 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** @var String */ + private $lang; + + /** + * @param $lang + * + * @return $this + */ + public function setLang($lang) + { + if (isset($lang) !== true) { + return $this; + } + + $this->lang = $lang; + + return $this; + } + + /** + * @param array $body + * + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->lang) !== true) { + throw new Exceptions\RuntimeException( + 'lang is required for Put' + ); + } + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for put' + ); + } + $id = $this->id; + $lang = $this->lang; + $uri = "/_scripts/$lang/$id"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'version_type', + 'version', + 'op_type' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Scroll.php b/Framework/lib/Elasticsearch/Endpoints/Scroll.php new file mode 100755 index 0000000..65aab8d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Scroll.php @@ -0,0 +1,98 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Scroll extends AbstractEndpoint +{ + private $clear = false; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return array + */ + public function getBody() + { + return $this->body; + } + + public function setClearScroll($clear) + { + $this->clear = $clear; + + return $this; + } + + /** + * @param $scroll_id + * + * @return $this + */ + public function setScrollId($scroll_id) + { + if (isset($scroll_id) !== true) { + return $this; + } + + $this->body = $scroll_id; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $uri = "/_search/scroll"; + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'scroll', + ); + } + + /** + * @return string + */ + public function getMethod() + { + if ($this->clear == true) { + return 'DELETE'; + } + + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Search.php b/Framework/lib/Elasticsearch/Endpoints/Search.php new file mode 100755 index 0000000..caf955e --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Search.php @@ -0,0 +1,107 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Search extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_search"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_search"; + } elseif (isset($index) === true) { + $uri = "/$index/_search"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_search"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'analyzer', + 'analyze_wildcard', + 'default_operator', + 'df', + 'explain', + 'from', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'indices_boost', + 'lenient', + 'lowercase_expanded_terms', + 'preference', + 'q', + 'query_cache', + 'request_cache', + 'routing', + 'scroll', + 'search_type', + 'size', + 'sort', + 'source', + '_source', + '_source_exclude', + '_source_include', + 'stats', + 'suggest_field', + 'suggest_mode', + 'suggest_size', + 'suggest_text', + 'timeout', + 'version', + 'fielddata_fields', + 'docvalue_fields', + 'filter_path' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/SearchShards.php b/Framework/lib/Elasticsearch/Endpoints/SearchShards.php new file mode 100755 index 0000000..85c564a --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/SearchShards.php @@ -0,0 +1,58 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class SearchShards extends AbstractEndpoint +{ + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_search_shards"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_search_shards"; + } elseif (isset($index) === true) { + $uri = "/$index/_search_shards"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_search_shards"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'preference', + 'routing', + 'local', + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/SearchTemplate.php b/Framework/lib/Elasticsearch/Endpoints/SearchTemplate.php new file mode 100755 index 0000000..7b5c830 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/SearchTemplate.php @@ -0,0 +1,79 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class SearchTemplate extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $type = $this->type; + $uri = "/_search/template"; + + if (isset($index) === true && isset($type) === true) { + $uri = "/$index/$type/_search/template"; + } elseif (isset($index) === true) { + $uri = "/$index/_search/template"; + } elseif (isset($type) === true) { + $uri = "/_all/$type/_search/template"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'preference', + 'routing', + 'scroll', + 'search_type' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Create.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Create.php new file mode 100755 index 0000000..a00a1ce --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Create.php @@ -0,0 +1,119 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Create extends AbstractEndpoint +{ + // A repository name + private $repository; + + // A snapshot name + private $snapshot; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @param $snapshot + * + * @return $this + */ + public function setSnapshot($snapshot) + { + if (isset($snapshot) !== true) { + return $this; + } + + $this->snapshot = $snapshot; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Create' + ); + } + if (isset($this->snapshot) !== true) { + throw new Exceptions\RuntimeException( + 'snapshot is required for Create' + ); + } + $repository = $this->repository; + $snapshot = $this->snapshot; + $uri = "/_snapshot/$repository/$snapshot"; + + if (isset($repository) === true && isset($snapshot) === true) { + $uri = "/_snapshot/$repository/$snapshot"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'wait_for_completion', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Delete.php new file mode 100755 index 0000000..ca28cfc --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Delete.php @@ -0,0 +1,101 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + // A repository name + private $repository; + + // A snapshot name + private $snapshot; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @param $snapshot + * + * @return $this + */ + public function setSnapshot($snapshot) + { + if (isset($snapshot) !== true) { + return $this; + } + + $this->snapshot = $snapshot; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Delete' + ); + } + if (isset($this->snapshot) !== true) { + throw new Exceptions\RuntimeException( + 'snapshot is required for Delete' + ); + } + $repository = $this->repository; + $snapshot = $this->snapshot; + $uri = "/_snapshot/$repository/$snapshot"; + + if (isset($repository) === true && isset($snapshot) === true) { + $uri = "/_snapshot/$repository/$snapshot"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Get.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Get.php new file mode 100755 index 0000000..70c6b54 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Get.php @@ -0,0 +1,102 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // A comma-separated list of repository names + private $repository; + + // A comma-separated list of snapshot names + private $snapshot; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @param $snapshot + * + * @return $this + */ + public function setSnapshot($snapshot) + { + if (isset($snapshot) !== true) { + return $this; + } + + $this->snapshot = $snapshot; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Get' + ); + } + if (isset($this->snapshot) !== true) { + throw new Exceptions\RuntimeException( + 'snapshot is required for Get' + ); + } + $repository = $this->repository; + $snapshot = $this->snapshot; + $uri = "/_snapshot/$repository/$snapshot"; + + if (isset($repository) === true && isset($snapshot) === true) { + $uri = "/_snapshot/$repository/$snapshot"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'ignore_unavailable' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Create.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Create.php new file mode 100755 index 0000000..94275c7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Create.php @@ -0,0 +1,107 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Create extends AbstractEndpoint +{ + // A repository name + private $repository; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Create' + ); + } + $repository = $this->repository; + $uri = "/_snapshot/$repository"; + + if (isset($repository) === true) { + $uri = "/_snapshot/$repository"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'timeout', + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Create Repository'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Delete.php new file mode 100755 index 0000000..4e0109f --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Delete.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + // A comma-separated list of repository names + private $repository; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Delete' + ); + } + $repository = $this->repository; + $uri = "/_snapshot/$repository"; + + if (isset($repository) === true) { + $uri = "/_snapshot/$repository"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'timeout', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Get.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Get.php new file mode 100755 index 0000000..57af42b --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Get.php @@ -0,0 +1,70 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + // A comma-separated list of repository names + private $repository; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $repository = $this->repository; + $uri = "/_snapshot"; + + if (isset($repository) === true) { + $uri = "/_snapshot/$repository"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Verify.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Verify.php new file mode 100755 index 0000000..ebd8fa3 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Repository/Verify.php @@ -0,0 +1,74 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Verify extends AbstractEndpoint +{ + // A comma-separated list of repository names + private $repository; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + $repository = $this->repository; + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Verify' + ); + } + + $uri = "/_snapshot/$repository/_verify"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'local', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Restore.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Restore.php new file mode 100755 index 0000000..193d203 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Restore.php @@ -0,0 +1,119 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Restore extends AbstractEndpoint +{ + // A repository name + private $repository; + + // A snapshot name + private $snapshot; + + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @param $snapshot + * + * @return $this + */ + public function setSnapshot($snapshot) + { + if (isset($snapshot) !== true) { + return $this; + } + + $this->snapshot = $snapshot; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'repository is required for Restore' + ); + } + if (isset($this->snapshot) !== true) { + throw new Exceptions\RuntimeException( + 'snapshot is required for Restore' + ); + } + $repository = $this->repository; + $snapshot = $this->snapshot; + $uri = "/_snapshot/$repository/$snapshot/_restore"; + + if (isset($repository) === true && isset($snapshot) === true) { + $uri = "/_snapshot/$repository/$snapshot/_restore"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'wait_for_completion', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Snapshot/Status.php b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Status.php new file mode 100755 index 0000000..b8e6aba --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Snapshot/Status.php @@ -0,0 +1,100 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Status extends AbstractEndpoint +{ + // A comma-separated list of repository names + private $repository; + + // A comma-separated list of snapshot names + private $snapshot; + + /** + * @param $repository + * + * @return $this + */ + public function setRepository($repository) + { + if (isset($repository) !== true) { + return $this; + } + + $this->repository = $repository; + + return $this; + } + + /** + * @param $snapshot + * + * @return $this + */ + public function setSnapshot($snapshot) + { + if (isset($snapshot) !== true) { + return $this; + } + + $this->snapshot = $snapshot; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->snapshot) === true && isset($this->repository) !== true) { + throw new Exceptions\RuntimeException( + 'Repository param must be provided if snapshot param is set' + ); + } + + $repository = $this->repository; + $snapshot = $this->snapshot; + $uri = "/_snapshot/_status"; + + if (isset($repository) === true) { + $uri = "/_snapshot/$repository/_status"; + } elseif (isset($repository) === true && isset($snapshot) === true) { + $uri = "/_snapshot/$repository/$snapshot/_status"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'master_timeout', + 'ignore_unavailable' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Source/Get.php b/Framework/lib/Elasticsearch/Endpoints/Source/Get.php new file mode 100755 index 0000000..0e8ac26 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Source/Get.php @@ -0,0 +1,78 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Get' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Get' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Get' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id/_source"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id/_source"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'parent', + 'preference', + 'realtime', + 'refresh', + 'routing', + '_source', + '_source_exclude', + '_source_include', + 'version', + 'version_type', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Suggest.php b/Framework/lib/Elasticsearch/Endpoints/Suggest.php new file mode 100755 index 0000000..658eead --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Suggest.php @@ -0,0 +1,85 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Suggest extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @return string + */ + public function getURI() + { + $index = $this->index; + $uri = "/_suggest"; + + if (isset($index) === true) { + $uri = "/$index/_suggest"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'ignore_unavailable', + 'allow_no_indices', + 'expand_wildcards', + 'preference', + 'routing', + 'source', + ); + } + + /** + * @return array + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + */ + public function getBody() + { + if (isset($this->body) !== true) { + throw new Exceptions\RuntimeException('Body is required for Suggest'); + } + + return $this->body; + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Tasks/Cancel.php b/Framework/lib/Elasticsearch/Endpoints/Tasks/Cancel.php new file mode 100755 index 0000000..ff2405b --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Tasks/Cancel.php @@ -0,0 +1,71 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Cancel extends AbstractEndpoint +{ + private $taskId; + + /** + * @param string $taskId + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setTaskId($taskId) + { + if (isset($taskId) !== true) { + return $this; + } + + $this->taskId = $taskId; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) === true) { + return "/_tasks/{$this->taskId}/_cancel"; + } + + return "/_tasks/_cancel"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'node_id', + 'actions', + 'parent_node', + 'parent_task', + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Tasks/Get.php b/Framework/lib/Elasticsearch/Endpoints/Tasks/Get.php new file mode 100755 index 0000000..4e7318d --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Tasks/Get.php @@ -0,0 +1,68 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + private $taskId; + + /** + * @param string $taskId + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setTaskId($taskId) + { + if (isset($taskId) !== true) { + return $this; + } + + $this->taskId = $taskId; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->taskId) === true) { + return "/_tasks/{$this->taskId}"; + } + + return "/_tasks"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'wait_for_completion' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Tasks/TasksList.php b/Framework/lib/Elasticsearch/Endpoints/Tasks/TasksList.php new file mode 100755 index 0000000..b45f206 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Tasks/TasksList.php @@ -0,0 +1,53 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class TasksList extends AbstractEndpoint +{ + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + return "/_tasks"; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'node_id', + 'actions', + 'detailed', + 'parent_node', + 'parent_task', + 'wait_for_completion', + 'group_by', + 'task_id' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Template/Delete.php b/Framework/lib/Elasticsearch/Endpoints/Template/Delete.php new file mode 100755 index 0000000..b3593ba --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Template/Delete.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Delete extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Delete' + ); + } + $templateId = $this->id; + $uri = "/_search/template/$templateId"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array(); + } + + /** + * @return string + */ + public function getMethod() + { + return 'DELETE'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Template/Get.php b/Framework/lib/Elasticsearch/Endpoints/Template/Get.php new file mode 100755 index 0000000..954ecdd --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Template/Get.php @@ -0,0 +1,51 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Get extends AbstractEndpoint +{ + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Get' + ); + } + $templateId = $this->id; + $uri = "/_search/template/$templateId"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array(); + } + + /** + * @return string + */ + public function getMethod() + { + return 'GET'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Template/Put.php b/Framework/lib/Elasticsearch/Endpoints/Template/Put.php new file mode 100755 index 0000000..075f413 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Template/Put.php @@ -0,0 +1,68 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Put extends AbstractEndpoint +{ + /** + * @param array $body + * + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Put' + ); + } + + $templateId = $this->id; + $uri = "/_search/template/$templateId"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array(); + } + + /** + * @return string + */ + public function getMethod() + { + return 'PUT'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/TermVectors.php b/Framework/lib/Elasticsearch/Endpoints/TermVectors.php new file mode 100755 index 0000000..5ac9569 --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/TermVectors.php @@ -0,0 +1,91 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class TermVectors extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for TermVectors' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for TermVectors' + ); + } + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for TermVectors' + ); + } + + $index = $this->index; + $type = $this->type; + $id = $this->id; + $uri = "/$index/$type/$id/_termvectors"; + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'term_statistics', + 'field_statistics', + 'fields', + 'offsets', + 'positions', + 'payloads', + 'preference', + 'routing', + 'parent', + 'realtime' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Endpoints/Update.php b/Framework/lib/Elasticsearch/Endpoints/Update.php new file mode 100755 index 0000000..9627fee --- /dev/null +++ b/Framework/lib/Elasticsearch/Endpoints/Update.php @@ -0,0 +1,99 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Update extends AbstractEndpoint +{ + /** + * @param array $body + * + * @throws \Elasticsearch\Common\Exceptions\InvalidArgumentException + * @return $this + */ + public function setBody($body) + { + if (isset($body) !== true) { + return $this; + } + + $this->body = $body; + + return $this; + } + + /** + * @throws \Elasticsearch\Common\Exceptions\RuntimeException + * @return string + */ + public function getURI() + { + if (isset($this->id) !== true) { + throw new Exceptions\RuntimeException( + 'id is required for Update' + ); + } + if (isset($this->index) !== true) { + throw new Exceptions\RuntimeException( + 'index is required for Update' + ); + } + if (isset($this->type) !== true) { + throw new Exceptions\RuntimeException( + 'type is required for Update' + ); + } + $id = $this->id; + $index = $this->index; + $type = $this->type; + $uri = "/$index/$type/$id/_update"; + + if (isset($index) === true && isset($type) === true && isset($id) === true) { + $uri = "/$index/$type/$id/_update"; + } + + return $uri; + } + + /** + * @return string[] + */ + public function getParamWhitelist() + { + return array( + 'consistency', + 'fields', + 'lang', + 'parent', + 'refresh', + 'replication', + 'retry_on_conflict', + 'routing', + 'script', + 'timeout', + 'timestamp', + 'ttl', + 'version', + 'version_type', + '_source' + ); + } + + /** + * @return string + */ + public function getMethod() + { + return 'POST'; + } +} diff --git a/Framework/lib/Elasticsearch/Helper/Iterators/SearchHitIterator.php b/Framework/lib/Elasticsearch/Helper/Iterators/SearchHitIterator.php new file mode 100755 index 0000000..38695c3 --- /dev/null +++ b/Framework/lib/Elasticsearch/Helper/Iterators/SearchHitIterator.php @@ -0,0 +1,161 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + * @see Iterator + */ +class SearchHitIterator implements Iterator, \Countable +{ + + /** + * @var SearchResponseIterator + */ + private $search_responses; + + /** + * @var int + */ + protected $current_key; + + /** + * @var int + */ + protected $current_hit_index; + + /** + * @var array|null + */ + protected $current_hit_data; + + /** + * @var int + */ + protected $count; + + /** + * Constructor + * + * @param SearchResponseIterator $search_responses + */ + public function __construct(SearchResponseIterator $search_responses) + { + $this->search_responses = $search_responses; + } + + /** + * Rewinds the internal SearchResponseIterator and itself + * + * @return void + * @see Iterator::rewind() + */ + public function rewind() + { + $this->current_key = 0; + $this->search_responses->rewind(); + + // The first page may be empty. In that case, the next page is fetched. + $current_page = $this->search_responses->current(); + if ($this->search_responses->valid() && empty($current_page['hits']['hits'])) { + $this->search_responses->next(); + } + + $this->count = 0; + if (isset($current_page['hits']) && isset($current_page['hits']['total'])) { + $this->count = $current_page['hits']['total']; + } + + $this->readPageData(); + } + + /** + * Advances pointer of the current hit to the next one in the current page. If there + * isn't a next hit in the current page, then it advances the current page and moves the + * pointer to the first hit in the page. + * + * @return void + * @see Iterator::next() + */ + public function next() + { + $this->current_key++; + $this->current_hit_index++; + $current_page = $this->search_responses->current(); + if (isset($current_page['hits']['hits'][$this->current_hit_index])) { + $this->current_hit_data = $current_page['hits']['hits'][$this->current_hit_index]; + } else { + $this->search_responses->next(); + $this->readPageData(); + } + } + + /** + * Returns a boolean indicating whether or not the current pointer has valid data + * + * @return bool + * @see Iterator::valid() + */ + public function valid() + { + return is_array($this->current_hit_data); + } + + /** + * Returns the current hit + * + * @return array + * @see Iterator::current() + */ + public function current() + { + return $this->current_hit_data; + } + + /** + * Returns the current hit index. The hit index spans all pages. + * + * @return int + * @see Iterator::key() + */ + public function key() + { + return $this->current_hit_index; + } + + /** + * Advances the internal SearchResponseIterator and resets the current_hit_index to 0 + * + * @internal + */ + private function readPageData() + { + if ($this->search_responses->valid()) { + $current_page = $this->search_responses->current(); + $this->current_hit_index = 0; + $this->current_hit_data = $current_page['hits']['hits'][$this->current_hit_index]; + } else { + $this->current_hit_data = null; + } + } + + /** + * {@inheritDoc} + */ + public function count() + { + if ($this->count === null) { + $this->rewind(); + } + + return $this->count; + } +} diff --git a/Framework/lib/Elasticsearch/Helper/Iterators/SearchResponseIterator.php b/Framework/lib/Elasticsearch/Helper/Iterators/SearchResponseIterator.php new file mode 100755 index 0000000..f864422 --- /dev/null +++ b/Framework/lib/Elasticsearch/Helper/Iterators/SearchResponseIterator.php @@ -0,0 +1,175 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + * @see Iterator + */ +class SearchResponseIterator implements Iterator +{ + + /** + * @var Client + */ + private $client; + + /** + * @var array + */ + private $params; + + /** + * @var int + */ + private $current_key; + + /** + * @var array + */ + private $current_scrolled_response; + + /** + * @var string + */ + private $scroll_id; + + /** + * @var duration + */ + private $scroll_ttl; + + /** + * Constructor + * + * @param Client $client + * @param array $params Associative array of parameters + * @see Client::search() + */ + public function __construct(Client $client, array $search_params) + { + $this->client = $client; + $this->params = $search_params; + + if (isset($search_params['scroll'])) { + $this->scroll_ttl = $search_params['scroll']; + } + } + + /** + * Destructor + */ + public function __destruct() + { + $this->clearScroll(); + } + + /** + * Sets the time to live duration of a scroll window + * + * @param string $time_to_live + * @return $this + */ + public function setScrollTimeout($time_to_live) + { + $this->scroll_ttl = $time_to_live; + return $this; + } + + /** + * Clears the current scroll window if there is a scroll_id stored + * + * @return void + */ + private function clearScroll() + { + if (!empty($this->scroll_id)) { + $this->client->clearScroll( + array( + 'scroll_id' => $this->scroll_id, + 'client' => array( + 'ignore' => 404 + ) + ) + ); + $this->scroll_id = null; + } + } + + /** + * Rewinds the iterator by performing the initial search. + * + * + * @return void + * @see Iterator::rewind() + */ + public function rewind() + { + $this->clearScroll(); + $this->current_key = 0; + $this->current_scrolled_response = $this->client->search($this->params); + $this->scroll_id = $this->current_scrolled_response['_scroll_id']; + } + + /** + * Fetches every "page" after the first one using the lastest "scroll_id" + * + * @return void + * @see Iterator::next() + */ + public function next() + { + if ($this->current_key !== 0) { + $this->current_scrolled_response = $this->client->scroll( + array( + 'scroll_id' => $this->scroll_id, + 'scroll' => $this->scroll_ttl + ) + ); + $this->scroll_id = $this->current_scrolled_response['_scroll_id']; + } + $this->current_key++; + } + + /** + * Returns a boolean value indicating if the current page is valid or not + * + * @return bool + * @see Iterator::valid() + */ + public function valid() + { + return isset($this->current_scrolled_response['hits']['hits'][0]); + } + + /** + * Returns the current "page" + * + * @return array + * @see Iterator::current() + */ + public function current() + { + return $this->current_scrolled_response; + } + + /** + * Returns the current "page number" of the current "page" + * + * @return int + * @see Iterator::key() + */ + public function key() + { + return $this->current_key; + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/AbstractNamespace.php b/Framework/lib/Elasticsearch/Namespaces/AbstractNamespace.php new file mode 100755 index 0000000..42a0c78 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/AbstractNamespace.php @@ -0,0 +1,77 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +abstract class AbstractNamespace +{ + /** @var \Elasticsearch\Transport */ + protected $transport; + + /** @var callback */ + protected $endpoints; + + /** + * Abstract constructor + * + * @param Transport $transport Transport object + * @param $endpoints + */ + public function __construct($transport, $endpoints) + { + $this->transport = $transport; + $this->endpoints = $endpoints; + } + + /** + * @param array $params + * @param string $arg + * + * @return null|mixed + */ + public function extractArgument(&$params, $arg) + { + if (is_object($params) === true) { + $params = (array) $params; + } + + if (isset($params[$arg]) === true) { + $val = $params[$arg]; + unset($params[$arg]); + + return $val; + } else { + return null; + } + } + + /** + * @param $endpoint AbstractEndpoint + * + * @throws \Exception + * @return array + */ + protected function performRequest(AbstractEndpoint $endpoint) + { + $response = $this->transport->performRequest( + $endpoint->getMethod(), + $endpoint->getURI(), + $endpoint->getParams(), + $endpoint->getBody(), + $endpoint->getOptions() + ); + + return $this->transport->resultOrFuture($response, $endpoint->getOptions()); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/BooleanRequestWrapper.php b/Framework/lib/Elasticsearch/Namespaces/BooleanRequestWrapper.php new file mode 100755 index 0000000..eb9c4cd --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/BooleanRequestWrapper.php @@ -0,0 +1,58 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +trait BooleanRequestWrapper +{ + /** + * Perform Request + * + * @param AbstractEndpoint $endpoint The Endpoint to perform this request against + * + * @throws Missing404Exception + * @throws RoutingMissingException + */ + public static function performRequest(AbstractEndpoint $endpoint, Transport $transport) + { + try { + $response = $transport->performRequest( + $endpoint->getMethod(), + $endpoint->getURI(), + $endpoint->getParams(), + $endpoint->getBody(), + $endpoint->getOptions() + ); + + $response = $transport->resultOrFuture($response, $endpoint->getOptions()); + if (!($response instanceof FutureArrayInterface)) { + if ($response['status'] === 200) { + return true; + } else { + return false; + } + } else { + // async mode, can't easily resolve this...punt to user + return $response; + } + } catch (Missing404Exception $exception) { + return false; + } catch (RoutingMissingException $exception) { + return false; + } + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/CatNamespace.php b/Framework/lib/Elasticsearch/Namespaces/CatNamespace.php new file mode 100755 index 0000000..dac0ff6 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/CatNamespace.php @@ -0,0 +1,493 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class CatNamespace extends AbstractNamespace +{ + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function aliases($params = array()) + { + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Aliases $endpoint */ + $endpoint = $endpointBuilder('Cat\Aliases'); + $endpoint->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function allocation($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Allocation $endpoint */ + $endpoint = $endpointBuilder('Cat\Allocation'); + $endpoint->setNodeID($nodeID); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function count($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Count $endpoint */ + $endpoint = $endpointBuilder('Cat\Count'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['ts'] = (bool) Set to false to disable timestamping + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function health($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Health $endpoint */ + $endpoint = $endpointBuilder('Cat\Health'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['help'] = (bool) Return help information + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function help($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Help $endpoint */ + $endpoint = $endpointBuilder('Cat\Help'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * ['pri'] = (bool) Set to true to return stats only for primary shards + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function indices($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Indices $endpoint */ + $endpoint = $endpointBuilder('Cat\Indices'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function master($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Master $endpoint */ + $endpoint = $endpointBuilder('Cat\Master'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function nodes($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Nodes $endpoint */ + $endpoint = $endpointBuilder('Cat\Nodes'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function nodeAttrs($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\NodeAttrs $endpoint */ + $endpoint = $endpointBuilder('Cat\NodeAttrs'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function pendingTasks($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\PendingTasks $endpoint */ + $endpoint = $endpointBuilder('Cat\PendingTasks'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function recovery($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Recovery $endpoint */ + $endpoint = $endpointBuilder('Cat\Recovery'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function repositories($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Repositories $endpoint */ + $endpoint = $endpointBuilder('Cat\Repositories'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function shards($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Shards $endpoint */ + $endpoint = $endpointBuilder('Cat\Shards'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * ['repository'] = (string) Name of repository from which to fetch the snapshot information + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function snapshots($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Snapshots $endpoint */ + $endpoint = $endpointBuilder('Cat\Snapshots'); + $endpoint->setRepository($repository); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['full_id'] = (bool) Enables displaying the complete node ids + * ['size'] = (enum) The multiplier in which to display values ([ "", "k", "m", "g", "t", "p" ]) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function threadPool($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\ThreadPool $endpoint */ + $endpoint = $endpointBuilder('Cat\ThreadPool'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * ['bytes'] = (enum) The unit in which to display byte values + * ['fields'] = (list) A comma-separated list of fields to return the fielddata size + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function fielddata($params = array()) + { + $fields = $this->extractArgument($params, 'fields'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Fielddata $endpoint */ + $endpoint = $endpointBuilder('Cat\Fielddata'); + $endpoint->setFields($fields); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function plugins($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Plugins $endpoint */ + $endpoint = $endpointBuilder('Cat\Plugins'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function segments($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Segments $endpoint */ + $endpoint = $endpointBuilder('Cat\Segments'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['format'] = (string) a short version of the Accept header, e.g. json, yaml + * ['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['format'] = (string) a short version of the Accept header, e.g. json, yaml + * ['actions'] = (list) A comma-separated list of actions that should be returned. Leave empty to return all. + * ['detailed'] = (boolean) Return detailed task information (default: false) + * ['parent_node'] = (string) Return tasks with specified parent node. + * ['parent_task'] = (number) Return tasks with specified parent task id. Set to -1 to return all. + * ['h'] = (list) Comma-separated list of column names to display + * ['help'] = (bool) Return help information + * ['v'] = (bool) Verbose mode. Display column headers + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function tasks($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cat\Tasks $endpoint */ + $endpoint = $endpointBuilder('Cat\Tasks'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/ClusterNamespace.php b/Framework/lib/Elasticsearch/Namespaces/ClusterNamespace.php new file mode 100755 index 0000000..01a5002 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/ClusterNamespace.php @@ -0,0 +1,205 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ClusterNamespace extends AbstractNamespace +{ + /** + * $params['index'] = (string) Limit the information returned to a specific index + * ['level'] = (enum) Specify the level of detail for returned information + * ['local'] = (boolean) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * ['wait_for_active_shards'] = (number) Wait until the specified number of shards is active + * ['wait_for_nodes'] = (number) Wait until the specified number of nodes is available + * ['wait_for_relocating_shards'] = (number) Wait until the specified number of relocating shards is finished + * ['wait_for_status'] = (enum) Wait until cluster is in a specific state + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function health($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Health $endpoint */ + $endpoint = $endpointBuilder('Cluster\Health'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['dry_run'] = (boolean) Simulate the operation only and return the resulting state + * ['filter_metadata'] = (boolean) Don't return cluster state metadata (default: false) + * ['body'] = (boolean) Don't return cluster state metadata (default: false) + * ['explain'] = (boolean) Return an explanation of why the commands can or cannot be executed + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function reroute($params = array()) + { + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Reroute $endpoint */ + $endpoint = $endpointBuilder('Cluster\Reroute'); + $endpoint->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['filter_blocks'] = (boolean) Do not return information about blocks + * ['filter_index_templates'] = (boolean) Do not return information about index templates + * ['filter_indices'] = (list) Limit returned metadata information to specific indices + * ['filter_metadata'] = (boolean) Do not return information about indices metadata + * ['filter_nodes'] = (boolean) Do not return information about nodes + * ['filter_routing_table'] = (boolean) Do not return information about shard allocation (`routing_table` and `routing_nodes`) + * ['local'] = (boolean) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Specify timeout for connection to master + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function state($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $metric = $this->extractArgument($params, 'metric'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\State $endpoint */ + $endpoint = $endpointBuilder('Cluster\State'); + $endpoint->setParams($params) + ->setIndex($index) + ->setMetric($metric); + + return $this->performRequest($endpoint); + } + + /** + * $params['flat_settings'] = (boolean) Return settings in flat format (default: false) + * ['human'] = (boolean) Whether to return time and byte values in human-readable format. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function stats($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Stats $endpoint */ + $endpoint = $endpointBuilder('Cluster\Stats'); + $endpoint->setNodeID($nodeID) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['body'] = () + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putSettings($params = array()) + { + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Settings\Put $endpoint */ + $endpoint = $endpointBuilder('Cluster\Settings\Put'); + $endpoint->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * @param array $params + * + * @return array + */ + public function getSettings($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Settings\Put $endpoint */ + $endpoint = $endpointBuilder('Cluster\Settings\Get'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['master_timeout'] = (time) Specify timeout for connection to master + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function pendingTasks($params = array()) + { + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\PendingTasks $endpoint */ + $endpoint = $endpointBuilder('Cluster\PendingTasks'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['include_yes_decisions'] = (bool) Return 'YES' decisions in explanation (default: false) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function allocationExplain($params = array()) + { + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\AllocationExplain $endpoint */ + $endpoint = $endpointBuilder('Cluster\AllocationExplain'); + $endpoint->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/IndicesNamespace.php b/Framework/lib/Elasticsearch/Namespaces/IndicesNamespace.php new file mode 100755 index 0000000..2ad6b15 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/IndicesNamespace.php @@ -0,0 +1,1163 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class IndicesNamespace extends AbstractNamespace +{ + /** + * $params['index'] = (list) A comma-separated list of indices to check (Required) + * + * @param $params array Associative array of parameters + * + * @return boolean + */ + public function exists($params) + { + $index = $this->extractArgument($params, 'index'); + + //manually make this verbose so we can check status code + $params['client']['verbose'] = true; + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Exists $endpoint */ + $endpoint = $endpointBuilder('Indices\Exists'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return BooleanRequestWrapper::performRequest($endpoint, $this->transport); + } + + /** + * $params['index'] = (list) A comma-separated list of indices to check (Required) + * ['feature'] = (list) A comma-separated list of features to return + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * ['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * + * @param $params array Associative array of parameters + * + * @return bool + */ + public function get($params) + { + $index = $this->extractArgument($params, 'index'); + $feature = $this->extractArgument($params, 'feature'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Get'); + $endpoint->setIndex($index) + ->setFeature($feature) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['operation_threading'] = () TODO: ? + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function segments($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Segments $endpoint */ + $endpoint = $endpointBuilder('Indices\Segments'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (string) The name of the template (Required) + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteTemplate($params) + { + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Template\Delete $endpoint */ + $endpoint = $endpointBuilder('Indices\Template\Delete'); + $endpoint->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of indices to delete; use `_all` or empty string to delete all indices + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function delete($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Delete $endpoint */ + $endpoint = $endpointBuilder('Indices\Delete'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['fields'] = (boolean) A comma-separated list of fields for `fielddata` metric (supports wildcards) + * ['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['indexing_types'] = (list) A comma-separated list of document types to include in the `indexing` statistics + * ['metric_family'] = (enum) Limit the information returned to a specific metric + * ['search_groups'] = (list) A comma-separated list of search groups to include in the `search` statistics + * ['all'] = (boolean) Return all available information + * ['clear'] = (boolean) Reset the default level of detail + * ['docs'] = (boolean) Return information about indexed and deleted documents + * ['fielddata'] = (boolean) Return information about field data + * ['filter_cache'] = (boolean) Return information about filter cache + * ['flush'] = (boolean) Return information about flush operations + * ['get'] = (boolean) Return information about get operations + * ['groups'] = (boolean) A comma-separated list of search groups for `search` statistics + * ['id_cache'] = (boolean) Return information about ID cache + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['indexing'] = (boolean) Return information about indexing operations + * ['merge'] = (boolean) Return information about merge operations + * ['refresh'] = (boolean) Return information about refresh operations + * ['search'] = (boolean) Return information about search operations; use the `groups` parameter to include information for specific search groups + * ['store'] = (boolean) Return information about the size of the index + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function stats($params = array()) + { + $metric = $this->extractArgument($params, 'metric'); + + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Stats $endpoint */ + $endpoint = $endpointBuilder('Indices\Stats'); + $endpoint->setIndex($index) + ->setMetric($metric); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['body'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putSettings($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Settings\Put $endpoint */ + $endpoint = $endpointBuilder('Indices\Settings\Put'); + $endpoint->setIndex($index) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function snapshotIndex($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Gateway\Snapshot $endpoint */ + $endpoint = $endpointBuilder('Indices\Gateway\Snapshot'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the source index to shrink + * ['target'] = (string) The name of the target index to shrink into + * ['timeout'] = (time) Explicit operation timeout + * ['master_timeout'] = (time) Specify timeout for connection to master + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function shrink($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $target = $this->extractArgument($params, 'target'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Shrink $endpoint */ + $endpoint = $endpointBuilder('Indices\Shrink'); + $endpoint->setIndex($index) + ->setTarget($target); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['type'] = (list) A comma-separated list of document types + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getMapping($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $type = $this->extractArgument($params, 'type'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Mapping\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Mapping\Get'); + $endpoint->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['type'] = (list) A comma-separated list of document types + * ['field'] = (list) A comma-separated list of document fields + * ['include_defaults'] = (bool) specifies default mapping values should be returned + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getFieldMapping($params = array()) + { + $index = $this->extractArgument($params, 'index'); + $type = $this->extractArgument($params, 'type'); + $fields = $this->extractArgument($params, 'fields'); + + if (!isset($fields)) { + $fields = $this->extractArgument($params, 'field'); + } + + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Mapping\GetField $endpoint */ + $endpoint = $endpointBuilder('Indices\Mapping\GetField'); + $endpoint->setIndex($index) + ->setType($type) + ->setFields($fields); + + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['force'] = (boolean) TODO: ? + * ['full'] = (boolean) TODO: ? + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function flush($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Flush $endpoint */ + $endpoint = $endpointBuilder('Indices\Flush'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['force'] = (boolean) TODO: ? + * ['full'] = (boolean) TODO: ? + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function flushSynced($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Flush $endpoint */ + $endpoint = $endpointBuilder('Indices\Flush'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + $endpoint->setSynced(true); + + return $this->performRequest($endpoint); + } + + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['operation_threading'] = () TODO: ? + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function refresh($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Refresh $endpoint */ + $endpoint = $endpointBuilder('Indices\Refresh'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['detailed'] = (bool) Whether to display detailed information about shard recovery + * ['active_only'] = (bool) Display only those recoveries that are currently on-going + * ['human'] = (bool) Whether to return time and byte values in human-readable format. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function recovery($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Flush $endpoint */ + $endpoint = $endpointBuilder('Indices\Recovery'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` to check the types across all indices (Required) + * ['type'] = (list) A comma-separated list of document types to check (Required) + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return boolean + */ + public function existsType($params) + { + $index = $this->extractArgument($params, 'index'); + + $type = $this->extractArgument($params, 'type'); + + //manually make this verbose so we can check status code + $params['client']['verbose'] = true; + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Type\Exists $endpoint */ + $endpoint = $endpointBuilder('Indices\Type\Exists'); + $endpoint->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return BooleanRequestWrapper::performRequest($endpoint, $this->transport); + } + + /** + * $params['index'] = (string) The name of the index with an alias + * ['name'] = (string) The name of the alias to be created or updated + * ['timeout'] = (time) Explicit timestamp for the document + * ['body'] = (time) Explicit timestamp for the document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putAlias($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Alias\Put $endpoint */ + $endpoint = $endpointBuilder('Indices\Alias\Put'); + $endpoint->setIndex($index) + ->setName($name) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (string) The name of the template (Required) + * ['order'] = (number) The order for this template when merging multiple matching ones (higher numbers are merged later, overriding the lower numbers) + * ['timeout'] = (time) Explicit operation timeout + * ['body'] = (time) Explicit operation timeout + * ['create'] = (bool) Whether the index template should only be added if new or can also replace an existing one + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putTemplate($params) + { + $name = $this->extractArgument($params, 'name'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Template\Put $endpoint */ + $endpoint = $endpointBuilder('Indices\Template\Put'); + $endpoint->setName($name) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to restrict the operation; use `_all` or empty string to perform the operation on all indices + * ['type'] = (list) A comma-separated list of document types to restrict the operation; leave empty to perform the operation on all types + * ['explain'] = (boolean) Return detailed information about the error + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['operation_threading'] = () TODO: ? + * ['source'] = (string) The URL-encoded query definition (instead of using the request body) + * ['body'] = (string) The URL-encoded query definition (instead of using the request body) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function validateQuery($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $type = $this->extractArgument($params, 'type'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Validate\Query $endpoint */ + $endpoint = $endpointBuilder('Indices\Validate\Query'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (list) A comma-separated list of alias names to return (Required) + * ['index'] = (list) A comma-separated list of index names to filter aliases + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['name'] = (list) A comma-separated list of alias names to return + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getAlias($params) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Alias\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Alias\Get'); + $endpoint->setIndex($index) + ->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` to perform the operation on all indices (Required) + * ['type'] = (string) The name of the document type + * ['ignore_conflicts'] = (boolean) Specify whether to ignore conflicts while updating the mapping (default: false) + * ['timeout'] = (time) Explicit operation timeout + * ['body'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putMapping($params) + { + $index = $this->extractArgument($params, 'index'); + + $type = $this->extractArgument($params, 'type'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Mapping\Put $endpoint */ + $endpoint = $endpointBuilder('Indices\Mapping\Put'); + $endpoint->setIndex($index) + ->setType($type) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` for all indices (Required) + * ['type'] = (string) The name of the document type to delete (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteMapping($params) + { + $index = $this->extractArgument($params, 'index'); + + $type = $this->extractArgument($params, 'type'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Mapping\Delete $endpoint */ + $endpoint = $endpointBuilder('Indices\Mapping\Delete'); + $endpoint->setIndex($index) + ->setType($type); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (string) The name of the template (Required) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getTemplate($params) + { + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Template\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Template\Get'); + $endpoint->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (string) The name of the template (Required) + * + * @param $params array Associative array of parameters + * + * @return boolean + */ + public function existsTemplate($params) + { + $name = $this->extractArgument($params, 'name'); + + //manually make this verbose so we can check status code + $params['client']['verbose'] = true; + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Template\Exists $endpoint */ + $endpoint = $endpointBuilder('Indices\Template\Exists'); + $endpoint->setName($name); + $endpoint->setParams($params); + + return BooleanRequestWrapper::performRequest($endpoint, $this->transport); + } + + /** + * $params['index'] = (string) The name of the index (Required) + * ['timeout'] = (time) Explicit operation timeout + * ['body'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function create($params) + { + $index = $this->extractArgument($params, 'index'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Create $endpoint */ + $endpoint = $endpointBuilder('Indices\Create'); + $endpoint->setIndex($index) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['flush'] = (boolean) Specify whether the index should be flushed after performing the operation (default: true) + * ['max_num_segments'] = (number) The number of segments the index should be merged into (default: dynamic) + * ['only_expunge_deletes'] = (boolean) Specify whether the operation should only expunge deleted documents + * ['operation_threading'] = () TODO: ? + * ['refresh'] = (boolean) Specify whether the index should be refreshed after performing the operation (default: true) + * ['wait_for_merge'] = (boolean) Specify whether the request should block until the merge process is finished (default: true) + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function forceMerge($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\ForceMerge $endpoint */ + $endpoint = $endpointBuilder('Indices\ForceMerge'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index with an alias (Required) + * ['name'] = (string) The name of the alias to be deleted (Required) + * ['timeout'] = (time) Explicit timestamp for the document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteAlias($params) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Alias\Delete $endpoint */ + $endpoint = $endpointBuilder('Indices\Alias\Delete'); + $endpoint->setIndex($index) + ->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index (Required) + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function open($params) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Open $endpoint */ + $endpoint = $endpointBuilder('Indices\Open'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index to scope the operation + * ['analyzer'] = (string) The name of the analyzer to use + * ['field'] = (string) Use the analyzer configured for this field (instead of passing the analyzer name) + * ['filter'] = (list) A comma-separated list of filters to use for the analysis + * ['prefer_local'] = (boolean) With `true`, specify that a local shard should be used if available, with `false`, use a random shard (default: true) + * ['text'] = (string) The text on which the analysis should be performed (when request body is not used) + * ['tokenizer'] = (string) The name of the tokenizer to use for the analysis + * ['format'] = (enum) Format of the output + * ['body'] = (enum) Format of the output + * ['char_filter'] = (list) A comma-separated list of character filters to use for the analysis + * ['explain'] = (bool) With `true`, outputs more advanced details. (default: false) + * ['attributes'] = (list) A comma-separated list of token attributes to output, this parameter works only with `explain=true` + * ['format'] = (enum) Format of the output (["detailed", "text"]) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function analyze($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Analyze $endpoint */ + $endpoint = $endpointBuilder('Indices\Analyze'); + $endpoint->setIndex($index) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index name to limit the operation + * ['field_data'] = (boolean) Clear field data + * ['fielddata'] = (boolean) Clear field data + * ['fields'] = (list) A comma-separated list of fields to clear when using the `field_data` parameter (default: all) + * ['filter'] = (boolean) Clear filter caches + * ['filter_cache'] = (boolean) Clear filter caches + * ['filter_keys'] = (boolean) A comma-separated list of keys to clear when using the `filter_cache` parameter (default: all) + * ['id'] = (boolean) Clear ID caches for parent/child + * ['id_cache'] = (boolean) Clear ID caches for parent/child + * ['recycler'] = (boolean) Clear the recycler cache + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function clearCache($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Cache\Clear $endpoint */ + $endpoint = $endpointBuilder('Indices\Cache\Clear'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names to filter aliases + * ['timeout'] = (time) Explicit timestamp for the document + * ['body'] = (time) Explicit timestamp for the document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function updateAliases($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Aliases\Update $endpoint */ + $endpoint = $endpointBuilder('Indices\Aliases\Update'); + $endpoint->setIndex($index) + ->setBody($body); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['local'] = (bool) Return local information, do not retrieve the state from master node (default: false) + * ['timeout'] = (time) Explicit timestamp for the document + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getAliases($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Aliases\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Aliases\Get'); + $endpoint->setIndex($index) + ->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['name'] = (list) A comma-separated list of alias names to return (Required) + * ['index'] = (list) A comma-separated list of index names to filter aliases + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return boolean + */ + public function existsAlias($params) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + //manually make this verbose so we can check status code + $params['client']['verbose'] = true; + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Alias\Exists $endpoint */ + $endpoint = $endpointBuilder('Indices\Alias\Exists'); + $endpoint->setIndex($index) + ->setName($name); + $endpoint->setParams($params); + + return BooleanRequestWrapper::performRequest($endpoint, $this->transport); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['ignore_indices'] = (enum) When performed on multiple indices, allows to ignore `missing` ones + * ['operation_threading'] = () TODO: ? + * ['recovery'] = (boolean) Return information about shard recovery + * ['snapshot'] = (boolean) TODO: ? + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function status($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Status $endpoint */ + $endpoint = $endpointBuilder('Indices\Status'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getSettings($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + $name = $this->extractArgument($params, 'name'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Settings\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Settings\Get'); + $endpoint->setIndex($index) + ->setName($name); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index (Required) + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function close($params) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Close $endpoint */ + $endpoint = $endpointBuilder('Indices\Close'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) The name of the index + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function seal($params) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Seal $endpoint */ + $endpoint = $endpointBuilder('Indices\Seal'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['wait_for_completion']= (boolean) Specify whether the request should block until the all segments are upgraded (default: false) + * ['only_ancient_segments'] = (boolean) If true, only ancient (an older Lucene major release) segments will be upgraded + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function upgrade($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Upgrade\Post $endpoint */ + $endpoint = $endpointBuilder('Indices\Upgrade\Post'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (list) A comma-separated list of index names; use `_all` or empty string for all indices + * ['wait_for_completion']= (boolean) Specify whether the request should block until the all segments are upgraded (default: false) + * ['only_ancient_segments'] = (boolean) If true, only ancient (an older Lucene major release) segments will be upgraded + * ['refresh'] = (boolean) Refresh the index after performing the operation + * ['ignore_unavailable'] = (bool) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (bool) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (enum) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getUpgrade($params = array()) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Upgrade\Get $endpoint */ + $endpoint = $endpointBuilder('Indices\Upgrade\Get'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['index'] = (string) A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices + * ['status'] = (list) A comma-separated list of statuses used to filter on shards to get store information for + * ['ignore_unavailable'] = (boolean) Whether specified concrete indices should be ignored when unavailable (missing or closed) + * ['allow_no_indices'] = (boolean) Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified) + * ['expand_wildcards'] = (boolean) Whether to expand wildcard expression to concrete indices that are open, closed or both. + * ['operation_threading'] + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function shardStores($params) + { + $index = $this->extractArgument($params, 'index'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\ShardStores $endpoint */ + $endpoint = $endpointBuilder('Indices\ShardStores'); + $endpoint->setIndex($index); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['newIndex'] = (string) The name of the rollover index + * ['alias'] = (string) The name of the alias to rollover + * ['timeout'] = (time) Explicit operation timeout + * ['master_timeout'] = (time) Specify timeout for connection to master + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function rollover($params) + { + $newIndex = $this->extractArgument($params, 'newIndex'); + $alias = $this->extractArgument($params, 'alias'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Indices\Rollover $endpoint */ + $endpoint = $endpointBuilder('Indices\Rollover'); + $endpoint->setNewIndex($newIndex) + ->setAlias($alias) + ->setParams($params) + ->setBody($body); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/IngestNamespace.php b/Framework/lib/Elasticsearch/Namespaces/IngestNamespace.php new file mode 100755 index 0000000..c14313b --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/IngestNamespace.php @@ -0,0 +1,114 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class IngestNamespace extends AbstractNamespace +{ + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deletePipeline($params = array()) + { + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Delete $endpoint */ + $endpoint = $endpointBuilder('Ingest\Pipeline\Delete'); + $endpoint->setID($id); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getPipeline($params = array()) + { + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Get $endpoint */ + $endpoint = $endpointBuilder('Ingest\Pipeline\Get'); + $endpoint->setID($id); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function putPipeline($params = array()) + { + $body = $this->extractArgument($params, 'body'); + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Put $endpoint */ + $endpoint = $endpointBuilder('Ingest\Pipeline\Put'); + $endpoint->setID($id) + ->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['verbose'] = (bool) Verbose mode. Display data output for each processor in executed pipeline + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function simulate($params = array()) + { + $body = $this->extractArgument($params, 'body'); + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Simulate $endpoint */ + $endpoint = $endpointBuilder('Ingest\Simulate'); + $endpoint->setID($id) + ->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/NamespaceBuilderInterface.php b/Framework/lib/Elasticsearch/Namespaces/NamespaceBuilderInterface.php new file mode 100755 index 0000000..7171dbb --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/NamespaceBuilderInterface.php @@ -0,0 +1,37 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ + +namespace Elasticsearch\Namespaces; + + +use Elasticsearch\Serializers\SerializerInterface; +use Elasticsearch\Transport; + +interface NamespaceBuilderInterface +{ + /** + * Returns the name of the namespace. This is what users will call, e.g. the name + * "foo" will be invoked by the user as `$client->foo()` + * @return string + */ + public function getName(); + + /** + * Returns the actual namespace object which contains your custom methods. The transport + * and serializer objects are provided so that your namespace may do whatever custom + * logic is required. + * + * @param Transport $transport + * @param SerializerInterface $serializer + * @return Object + */ + public function getObject(Transport $transport, SerializerInterface $serializer); +} \ No newline at end of file diff --git a/Framework/lib/Elasticsearch/Namespaces/NodesNamespace.php b/Framework/lib/Elasticsearch/Namespaces/NodesNamespace.php new file mode 100755 index 0000000..e8bbaf9 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/NodesNamespace.php @@ -0,0 +1,134 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class NodesNamespace extends AbstractNamespace +{ + /** + * $params['fields'] = (list) A comma-separated list of fields for `fielddata` metric (supports wildcards) + * ['metric_family'] = (enum) Limit the information returned to a certain metric family + * ['metric'] = (enum) Limit the information returned for `indices` family to a specific metric + * ['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['all'] = (boolean) Return all available information + * ['clear'] = (boolean) Reset the default level of detail + * ['fs'] = (boolean) Return information about the filesystem + * ['http'] = (boolean) Return information about HTTP + * ['indices'] = (boolean) Return information about indices + * ['jvm'] = (boolean) Return information about the JVM + * ['network'] = (boolean) Return information about network + * ['os'] = (boolean) Return information about the operating system + * ['process'] = (boolean) Return information about the Elasticsearch process + * ['thread_pool'] = (boolean) Return information about the thread pool + * ['transport'] = (boolean) Return information about transport + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function stats($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + + $metric = $this->extractArgument($params, 'metric'); + + $index_metric = $this->extractArgument($params, 'index_metric'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Nodes\Stats $endpoint */ + $endpoint = $endpointBuilder('Cluster\Nodes\Stats'); + $endpoint->setNodeID($nodeID) + ->setMetric($metric) + ->setIndexMetric($index_metric) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['metric'] = (list) A comma-separated list of metrics you wish returned. Leave empty to return all. + * ['flat_settings'] = (boolean) Return settings in flat format (default: false) + * ['human'] = (boolean) Whether to return time and byte values in human-readable format. + + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function info($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + $metric = $this->extractArgument($params, 'metric'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Nodes\Info $endpoint */ + $endpoint = $endpointBuilder('Cluster\Nodes\Info'); + $endpoint->setNodeID($nodeID)->setMetric($metric); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['interval'] = (time) The interval for the second sampling of threads + * ['snapshots'] = (number) Number of samples of thread stacktrace (default: 10) + * ['threads'] = (number) Specify the number of threads to provide information for (default: 3) + * ['type'] = (enum) The type to sample (default: cpu) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function hotThreads($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Nodes\HotThreads $endpoint */ + $endpoint = $endpointBuilder('Cluster\Nodes\HotThreads'); + $endpoint->setNodeID($nodeID); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['node_id'] = (list) A comma-separated list of node IDs or names to perform the operation on; use `_local` to perform the operation on the node you're connected to, leave empty to perform the operation on all nodes + * ['delay'] = (time) Set the delay for the operation (default: 1s) + * ['exit'] = (boolean) Exit the JVM as well (default: true) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function shutdown($params = array()) + { + $nodeID = $this->extractArgument($params, 'node_id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Cluster\Nodes\Shutdown $endpoint */ + $endpoint = $endpointBuilder('Cluster\Nodes\Shutdown'); + $endpoint->setNodeID($nodeID); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/SnapshotNamespace.php b/Framework/lib/Elasticsearch/Namespaces/SnapshotNamespace.php new file mode 100755 index 0000000..6f22d94 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/SnapshotNamespace.php @@ -0,0 +1,235 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class SnapshotNamespace extends AbstractNamespace +{ + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['wait_for_completion'] = (bool) Should this request wait until the operation has completed before returning + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function create($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $snapshot = $this->extractArgument($params, 'snapshot'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Create $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Create'); + $endpoint->setRepository($repository) + ->setSnapshot($snapshot) + ->setParams($params) + ->setBody($body); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function createRepository($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Repository\Create $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Repository\Create'); + $endpoint->setRepository($repository) + ->setBody($body) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function delete($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $snapshot = $this->extractArgument($params, 'snapshot'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Delete $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Delete'); + $endpoint->setRepository($repository) + ->setSnapshot($snapshot) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function deleteRepository($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Repository\Delete $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Repository\Delete'); + $endpoint->setRepository($repository) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function get($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $snapshot = $this->extractArgument($params, 'snapshot'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Get $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Get'); + $endpoint->setRepository($repository) + ->setSnapshot($snapshot) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function getRepository($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Repository\Get $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Repository\Get'); + $endpoint->setRepository($repository) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['wait_for_completion'] = (bool) Should this request wait until the operation has completed before returning + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function restore($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $snapshot = $this->extractArgument($params, 'snapshot'); + $body = $this->extractArgument($params, 'body'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Restore $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Restore'); + $endpoint->setRepository($repository) + ->setSnapshot($snapshot) + ->setParams($params) + ->setBody($body); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function status($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + $snapshot = $this->extractArgument($params, 'snapshot'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Status $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Status'); + $endpoint->setRepository($repository) + ->setSnapshot($snapshot) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['master_timeout'] = (time) Explicit operation timeout for connection to master node + * ['timeout'] = (time) Explicit operation timeout + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function verifyRepository($params = array()) + { + $repository = $this->extractArgument($params, 'repository'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var \Elasticsearch\Endpoints\Snapshot\Repository\Verify $endpoint */ + $endpoint = $endpointBuilder('Snapshot\Repository\Verify'); + $endpoint->setRepository($repository) + ->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Namespaces/TasksNamespace.php b/Framework/lib/Elasticsearch/Namespaces/TasksNamespace.php new file mode 100755 index 0000000..6782292 --- /dev/null +++ b/Framework/lib/Elasticsearch/Namespaces/TasksNamespace.php @@ -0,0 +1,91 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class TasksNamespace extends AbstractNamespace +{ + /** + * $params['wait_for_completion'] = (bool) Wait for the matching tasks to complete (default: false) + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function get($params = array()) + { + $id = $this->extractArgument($params, 'task_id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Get $endpoint */ + $endpoint = $endpointBuilder('Tasks\Get'); + $endpoint->setTaskId($id) + ->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['actions'] = (list) A comma-separated list of actions that should be cancelled. Leave empty to cancel all. + * ['parent_node'] = (string) Cancel tasks with specified parent node + * ['parent_task'] = (string) Cancel tasks with specified parent task id (node_id:task_number). Set to -1 to cancel all. + * ['detailed'] = (bool) Return detailed task information (default: false) + * ['wait_for_completion'] = (bool) Wait for the matching tasks to complete (default: false) + * ['group_by'] = (enum) Group tasks by nodes or parent/child relationships + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function tasksList($params = array()) + { + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Get $endpoint */ + $endpoint = $endpointBuilder('Tasks\TasksList'); + $endpoint->setParams($params); + + return $this->performRequest($endpoint); + } + + /** + * $params['node_id'] = (list) A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes + * ['actions'] = (list) A comma-separated list of actions that should be cancelled. Leave empty to cancel all. + * ['parent_node'] = (string) Cancel tasks with specified parent node + * ['parent_task'] = (string) Cancel tasks with specified parent task id (node_id:task_number). Set to -1 to cancel all. + * + * @param $params array Associative array of parameters + * + * @return array + */ + public function cancel($params = array()) + { + $id = $this->extractArgument($params, 'id'); + + /** @var callback $endpointBuilder */ + $endpointBuilder = $this->endpoints; + + /** @var Cancel $endpoint */ + $endpoint = $endpointBuilder('Tasks\Cancel'); + $endpoint->setTaskId($id) + ->setParams($params); + + return $this->performRequest($endpoint); + } +} diff --git a/Framework/lib/Elasticsearch/Serializers/ArrayToJSONSerializer.php b/Framework/lib/Elasticsearch/Serializers/ArrayToJSONSerializer.php new file mode 100755 index 0000000..1290b6b --- /dev/null +++ b/Framework/lib/Elasticsearch/Serializers/ArrayToJSONSerializer.php @@ -0,0 +1,49 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class ArrayToJSONSerializer implements SerializerInterface +{ + /** + * Serialize assoc array into JSON string + * + * @param string|array $data Assoc array to encode into JSON + * + * @return string + */ + public function serialize($data) + { + if (is_string($data) === true) { + return $data; + } else { + $data = json_encode($data, JSON_PRESERVE_ZERO_FRACTION); + if ($data === '[]') { + return '{}'; + } else { + return $data; + } + } + } + + /** + * Deserialize JSON into an assoc array + * + * @param string $data JSON encoded string + * @param array $headers Response Headers + * + * @return array + */ + public function deserialize($data, $headers) + { + return json_decode($data, true); + } +} diff --git a/Framework/lib/Elasticsearch/Serializers/EverythingToJSONSerializer.php b/Framework/lib/Elasticsearch/Serializers/EverythingToJSONSerializer.php new file mode 100755 index 0000000..b93b1bb --- /dev/null +++ b/Framework/lib/Elasticsearch/Serializers/EverythingToJSONSerializer.php @@ -0,0 +1,45 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class EverythingToJSONSerializer implements SerializerInterface +{ + /** + * Serialize assoc array into JSON string + * + * @param string|array $data Assoc array to encode into JSON + * + * @return string + */ + public function serialize($data) + { + $data = json_encode($data, JSON_PRESERVE_ZERO_FRACTION); + if ($data === '[]') { + return '{}'; + } else { + return $data; + } + } + + /** + * Deserialize JSON into an assoc array + * + * @param string $data JSON encoded string + * @param array $headers Response headers + * + * @return array + */ + public function deserialize($data, $headers) + { + return json_decode($data, true); + } +} diff --git a/Framework/lib/Elasticsearch/Serializers/SerializerInterface.php b/Framework/lib/Elasticsearch/Serializers/SerializerInterface.php new file mode 100755 index 0000000..a237963 --- /dev/null +++ b/Framework/lib/Elasticsearch/Serializers/SerializerInterface.php @@ -0,0 +1,34 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +interface SerializerInterface +{ + /** + * Serialize a complex data-structure into a json encoded string + * + * @param mixed The data to encode + * + * @return string + */ + public function serialize($data); + + /** + * Deserialize json encoded string into an associative array + * + * @param string $data JSON encoded string + * @param array $headers Response Headers + * + * @return array + */ + public function deserialize($data, $headers); +} diff --git a/Framework/lib/Elasticsearch/Serializers/SmartSerializer.php b/Framework/lib/Elasticsearch/Serializers/SmartSerializer.php new file mode 100755 index 0000000..bdae9f8 --- /dev/null +++ b/Framework/lib/Elasticsearch/Serializers/SmartSerializer.php @@ -0,0 +1,88 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class SmartSerializer implements SerializerInterface +{ + /** + * Serialize assoc array into JSON string + * + * @param string|array $data Assoc array to encode into JSON + * + * @return string + */ + public function serialize($data) + { + if (is_string($data) === true) { + return $data; + } else { + $data = json_encode($data, JSON_PRESERVE_ZERO_FRACTION); + if ($data === '[]') { + return '{}'; + } else { + return $data; + } + } + } + + /** + * Deserialize by introspecting content_type. Tries to deserialize JSON, + * otherwise returns string + * + * @param string $data JSON encoded string + * @param array $headers Response Headers + * + * @throws JsonErrorException + * @return array + */ + public function deserialize($data, $headers) + { + if (isset($headers['content_type']) === true) { + if (strpos($headers['content_type'], 'json') !== false) { + return $this->decode($data); + } else { + //Not json, return as string + return $data; + } + } else { + //No content headers, assume json + return $this->decode($data); + } + } + + /** + * @todo For 2.0, remove the E_NOTICE check before raising the exception. + * + * @param $data + * + * @return array + * @throws JsonErrorException + */ + private function decode($data) + { + if ($data === null || strlen($data) === 0) { + return ""; + } + + $result = @json_decode($data, true); + + // Throw exception only if E_NOTICE is on to maintain backwards-compatibility on systems that silently ignore E_NOTICEs. + if (json_last_error() !== JSON_ERROR_NONE && (error_reporting() & E_NOTICE) === E_NOTICE) { + $e = new JsonErrorException(json_last_error(), $data, $result); + throw $e; + } + + return $result; + } +} diff --git a/Framework/lib/Elasticsearch/Transport.php b/Framework/lib/Elasticsearch/Transport.php new file mode 100755 index 0000000..07323f7 --- /dev/null +++ b/Framework/lib/Elasticsearch/Transport.php @@ -0,0 +1,172 @@ + + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache2 + * @link http://elastic.co + */ +class Transport +{ + /** + * @var AbstractConnectionPool + */ + public $connectionPool; + + /** + * @var LoggerInterface + */ + private $log; + + /** @var int */ + public $retryAttempts = 0; + + /** @var Connection */ + public $lastConnection; + + /** @var int */ + public $retries; + + /** + * Transport class is responsible for dispatching requests to the + * underlying cluster connections + * + * @param $retries + * @param bool $sniffOnStart + * @param ConnectionPool\AbstractConnectionPool $connectionPool + * @param \Psr\Log\LoggerInterface $log Monolog logger object + */ + public function __construct($retries, $sniffOnStart = false, AbstractConnectionPool $connectionPool, LoggerInterface $log) + { + $this->log = $log; + $this->connectionPool = $connectionPool; + $this->retries = $retries; + + if ($sniffOnStart === true) { + $this->log->notice('Sniff on Start.'); + $this->connectionPool->scheduleCheck(); + } + } + + /** + * Returns a single connection from the connection pool + * Potentially performs a sniffing step before returning + * + * @return ConnectionInterface Connection + */ + + public function getConnection() + { + return $this->connectionPool->nextConnection(); + } + + /** + * Perform a request to the Cluster + * + * @param string $method HTTP method to use + * @param string $uri HTTP URI to send request to + * @param null $params Optional query parameters + * @param null $body Optional query body + * @param array $options + * + * @throws Common\Exceptions\NoNodesAvailableException|\Exception + * @return FutureArrayInterface + */ + public function performRequest($method, $uri, $params = null, $body = null, $options = []) + { + try { + $connection = $this->getConnection(); + } catch (Exceptions\NoNodesAvailableException $exception) { + $this->log->critical('No alive nodes found in cluster'); + throw $exception; + } + + $response = array(); + $caughtException = null; + $this->lastConnection = $connection; + + $future = $connection->performRequest( + $method, + $uri, + $params, + $body, + $options, + $this + ); + + $future->promise()->then( + //onSuccess + function ($response) { + $this->retryAttempts = 0; + // Note, this could be a 4xx or 5xx error + }, + //onFailure + function ($response) { + //some kind of real faiure here, like a timeout + $this->connectionPool->scheduleCheck(); + // log stuff + }); + + return $future; + } + + /** + * @param FutureArrayInterface $result Response of a request (promise) + * @param array $options Options for transport + * + * @return callable|array + */ + public function resultOrFuture($result, $options = []) + { + $response = null; + $async = isset($options['client']['future']) ? $options['client']['future'] : null; + if (is_null($async) || $async === false) { + do { + $result = $result->wait(); + } while ($result instanceof FutureArrayInterface); + + return $result; + } elseif ($async === true || $async === 'lazy') { + return $result; + } + } + + /** + * @param $request + * + * @return bool + */ + public function shouldRetry($request) + { + if ($this->retryAttempts < $this->retries) { + $this->retryAttempts += 1; + + return true; + } + + return false; + } + + /** + * Returns the last used connection so that it may be inspected. Mainly + * for debugging/testing purposes. + * + * @return Connection + */ + public function getLastConnection() + { + return $this->lastConnection; + } +} diff --git a/Framework/lib/Log/AbstractLogger.php b/Framework/lib/Log/AbstractLogger.php new file mode 100755 index 0000000..90e721a --- /dev/null +++ b/Framework/lib/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/Framework/lib/Log/InvalidArgumentException.php b/Framework/lib/Log/InvalidArgumentException.php new file mode 100755 index 0000000..67f852d --- /dev/null +++ b/Framework/lib/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/Framework/lib/Log/LoggerInterface.php b/Framework/lib/Log/LoggerInterface.php new file mode 100755 index 0000000..5ea7243 --- /dev/null +++ b/Framework/lib/Log/LoggerInterface.php @@ -0,0 +1,123 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/Framework/lib/Log/NullLogger.php b/Framework/lib/Log/NullLogger.php new file mode 100755 index 0000000..d8cd682 --- /dev/null +++ b/Framework/lib/Log/NullLogger.php @@ -0,0 +1,28 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/Framework/lib/Promise/CancellablePromiseInterface.php b/Framework/lib/Promise/CancellablePromiseInterface.php new file mode 100755 index 0000000..896db2d --- /dev/null +++ b/Framework/lib/Promise/CancellablePromiseInterface.php @@ -0,0 +1,11 @@ +started) { + return; + } + + $this->started = true; + $this->drain(); + } + + public function enqueue($cancellable) + { + if (!method_exists($cancellable, 'then') || !method_exists($cancellable, 'cancel')) { + return; + } + + $length = array_push($this->queue, $cancellable); + + if ($this->started && 1 === $length) { + $this->drain(); + } + } + + private function drain() + { + for ($i = key($this->queue); isset($this->queue[$i]); $i++) { + $cancellable = $this->queue[$i]; + + $exception = null; + + try { + $cancellable->cancel(); + } catch (\Throwable $exception) { + } catch (\Exception $exception) { + } + + unset($this->queue[$i]); + + if ($exception) { + throw $exception; + } + } + + $this->queue = []; + } +} diff --git a/Framework/lib/Promise/Deferred.php b/Framework/lib/Promise/Deferred.php new file mode 100755 index 0000000..96b93e6 --- /dev/null +++ b/Framework/lib/Promise/Deferred.php @@ -0,0 +1,42 @@ +canceller = $canceller; + } + + public function promise() + { + if (null === $this->promise) { + $this->promise = new Promise(function ($resolve, $reject) { + $this->resolveCallback = $resolve; + $this->rejectCallback = $reject; + }, $this->canceller); + } + + return $this->promise; + } + + public function resolve($value = null) + { + $this->promise(); + + call_user_func($this->resolveCallback, $value); + } + + public function reject($reason = null) + { + $this->promise(); + + call_user_func($this->rejectCallback, $reason); + } +} diff --git a/Framework/lib/Promise/Exception/LengthException.php b/Framework/lib/Promise/Exception/LengthException.php new file mode 100755 index 0000000..775c48d --- /dev/null +++ b/Framework/lib/Promise/Exception/LengthException.php @@ -0,0 +1,7 @@ +value = $value; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onFulfilled) { + return $this; + } + + return new Promise(function (callable $resolve, callable $reject) use ($onFulfilled) { + queue()->enqueue(function () use ($resolve, $reject, $onFulfilled) { + try { + $resolve($onFulfilled($this->value)); + } catch (\Throwable $exception) { + $reject($exception); + } catch (\Exception $exception) { + $reject($exception); + } + }); + }); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onFulfilled) { + return; + } + + queue()->enqueue(function () use ($onFulfilled) { + $result = $onFulfilled($this->value); + + if ($result instanceof ExtendedPromiseInterface) { + $result->done(); + } + }); + } + + public function otherwise(callable $onRejected) + { + return $this; + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(function ($value) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($value) { + return $value; + }); + }); + } + + public function cancel() + { + } +} diff --git a/Framework/lib/Promise/LazyPromise.php b/Framework/lib/Promise/LazyPromise.php new file mode 100755 index 0000000..8e08322 --- /dev/null +++ b/Framework/lib/Promise/LazyPromise.php @@ -0,0 +1,54 @@ +factory = $factory; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + return $this->promise()->then($onFulfilled, $onRejected); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null) + { + return $this->promise()->done($onFulfilled, $onRejected); + } + + public function otherwise(callable $onRejected) + { + return $this->promise()->otherwise($onRejected); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->promise()->always($onFulfilledOrRejected); + } + + public function cancel() + { + return $this->promise()->cancel(); + } + + private function promise() + { + if (null === $this->promise) { + try { + $this->promise = resolve(call_user_func($this->factory)); + } catch (\Throwable $exception) { + $this->promise = new RejectedPromise($exception); + } catch (\Exception $exception) { + $this->promise = new RejectedPromise($exception); + } + } + + return $this->promise; + } +} diff --git a/Framework/lib/Promise/Promise.php b/Framework/lib/Promise/Promise.php new file mode 100755 index 0000000..d09343f --- /dev/null +++ b/Framework/lib/Promise/Promise.php @@ -0,0 +1,157 @@ +canceller = $canceller; + $this->call($resolver); + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null !== $this->result) { + return $this->result()->then($onFulfilled, $onRejected); + } + + if (null === $this->canceller) { + return new static($this->resolver($onFulfilled, $onRejected)); + } + + $this->requiredCancelRequests++; + + return new static($this->resolver($onFulfilled, $onRejected), function () { + if (++$this->cancelRequests < $this->requiredCancelRequests) { + return; + } + + $this->cancel(); + }); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null) + { + if (null !== $this->result) { + return $this->result()->done($onFulfilled, $onRejected); + } + + $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { + $promise + ->done($onFulfilled, $onRejected); + }; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, function ($reason) use ($onRejected) { + if (!_checkTypehint($onRejected, $reason)) { + return new RejectedPromise($reason); + } + + return $onRejected($reason); + }); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(function ($value) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($value) { + return $value; + }); + }, function ($reason) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return new RejectedPromise($reason); + }); + }); + } + + public function cancel() + { + if (null === $this->canceller || null !== $this->result) { + return; + } + + $canceller = $this->canceller; + $this->canceller = null; + + $this->call($canceller); + } + + private function resolver(callable $onFulfilled = null, callable $onRejected = null) + { + return function ($resolve, $reject) use ($onFulfilled, $onRejected) { + $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject) { + $promise + ->then($onFulfilled, $onRejected) + ->done($resolve, $reject); + }; + }; + } + + private function resolve($value = null) + { + if (null !== $this->result) { + return; + } + + $this->settle(resolve($value)); + } + + private function reject($reason = null) + { + if (null !== $this->result) { + return; + } + + $this->settle(reject($reason)); + } + + private function settle(ExtendedPromiseInterface $result) + { + $handlers = $this->handlers; + + $this->handlers = []; + $this->result = $result; + + foreach ($handlers as $handler) { + $handler($result); + } + } + + private function result() + { + while ($this->result instanceof self && null !== $this->result->result) { + $this->result = $this->result->result; + } + + return $this->result; + } + + private function call(callable $callback) + { + try { + $callback( + function ($value = null) { + $this->resolve($value); + }, + function ($reason = null) { + $this->reject($reason); + } + ); + } catch (\Throwable $e) { + $this->reject($e); + } catch (\Exception $e) { + $this->reject($e); + } + } +} diff --git a/Framework/lib/Promise/PromiseInterface.php b/Framework/lib/Promise/PromiseInterface.php new file mode 100755 index 0000000..da138e8 --- /dev/null +++ b/Framework/lib/Promise/PromiseInterface.php @@ -0,0 +1,11 @@ +queue, $task)) { + $this->drain(); + } + } + + private function drain() + { + for ($i = key($this->queue); isset($this->queue[$i]); $i++) { + $task = $this->queue[$i]; + + $exception = null; + + try { + $task(); + } catch (\Throwable $exception) { + } catch (\Exception $exception) { + } + + unset($this->queue[$i]); + + if ($exception) { + throw $exception; + } + } + + $this->queue = []; + } +} diff --git a/Framework/lib/Promise/RejectedPromise.php b/Framework/lib/Promise/RejectedPromise.php new file mode 100755 index 0000000..6ea75fc --- /dev/null +++ b/Framework/lib/Promise/RejectedPromise.php @@ -0,0 +1,77 @@ +reason = $reason; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null) + { + if (null === $onRejected) { + return $this; + } + + return new Promise(function (callable $resolve, callable $reject) use ($onRejected) { + queue()->enqueue(function () use ($resolve, $reject, $onRejected) { + try { + $resolve($onRejected($this->reason)); + } catch (\Throwable $exception) { + $reject($exception); + } catch (\Exception $exception) { + $reject($exception); + } + }); + }); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null) + { + queue()->enqueue(function () use ($onRejected) { + if (null === $onRejected) { + throw UnhandledRejectionException::resolve($this->reason); + } + + $result = $onRejected($this->reason); + + if ($result instanceof self) { + throw UnhandledRejectionException::resolve($result->reason); + } + + if ($result instanceof ExtendedPromiseInterface) { + $result->done(); + } + }); + } + + public function otherwise(callable $onRejected) + { + if (!_checkTypehint($onRejected, $this->reason)) { + return $this; + } + + return $this->then(null, $onRejected); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(null, function ($reason) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return new RejectedPromise($reason); + }); + }); + } + + public function cancel() + { + } +} diff --git a/Framework/lib/Promise/UnhandledRejectionException.php b/Framework/lib/Promise/UnhandledRejectionException.php new file mode 100755 index 0000000..a44b7a1 --- /dev/null +++ b/Framework/lib/Promise/UnhandledRejectionException.php @@ -0,0 +1,31 @@ +reason = $reason; + + $message = sprintf('Unhandled Rejection: %s', json_encode($reason)); + + parent::__construct($message, 0); + } + + public function getReason() + { + return $this->reason; + } +} diff --git a/Framework/lib/Promise/functions.php b/Framework/lib/Promise/functions.php new file mode 100755 index 0000000..006e520 --- /dev/null +++ b/Framework/lib/Promise/functions.php @@ -0,0 +1,234 @@ +then($resolve, $reject); + }, $canceller); + } + + return new FulfilledPromise($promiseOrValue); +} + +function reject($promiseOrValue = null) +{ + if ($promiseOrValue instanceof PromiseInterface) { + return resolve($promiseOrValue)->then(function ($value) { + return new RejectedPromise($value); + }); + } + + return new RejectedPromise($promiseOrValue); +} + +function all(array $promisesOrValues) +{ + return map($promisesOrValues, function ($val) { + return $val; + }); +} + +function race(array $promisesOrValues) +{ + if (!$promisesOrValues) { + return resolve(); + } + + $cancellationQueue = new CancellationQueue(); + + return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue) { + foreach ($promisesOrValues as $promiseOrValue) { + $cancellationQueue->enqueue($promiseOrValue); + + resolve($promiseOrValue) + ->done($resolve, $reject); + } + }, $cancellationQueue); +} + +function any(array $promisesOrValues) +{ + return some($promisesOrValues, 1) + ->then(function ($val) { + return array_shift($val); + }); +} + +function some(array $promisesOrValues, $howMany) +{ + if ($howMany < 1) { + return resolve([]); + } + + $len = count($promisesOrValues); + + if ($len < $howMany) { + return reject( + new Exception\LengthException( + sprintf( + 'Input array must contain at least %d item%s but contains only %s item%s.', + $howMany, + 1 === $howMany ? '' : 's', + $len, + 1 === $len ? '' : 's' + ) + ) + ); + } + + $cancellationQueue = new CancellationQueue(); + + return new Promise(function ($resolve, $reject) use ($len, $promisesOrValues, $howMany, $cancellationQueue) { + $toResolve = $howMany; + $toReject = ($len - $toResolve) + 1; + $values = []; + $reasons = []; + + foreach ($promisesOrValues as $i => $promiseOrValue) { + $fulfiller = function ($val) use ($i, &$values, &$toResolve, $toReject, $resolve) { + if ($toResolve < 1 || $toReject < 1) { + return; + } + + $values[$i] = $val; + + if (0 === --$toResolve) { + $resolve($values); + } + }; + + $rejecter = function ($reason) use ($i, &$reasons, &$toReject, $toResolve, $reject) { + if ($toResolve < 1 || $toReject < 1) { + return; + } + + $reasons[$i] = $reason; + + if (0 === --$toReject) { + $reject($reasons); + } + }; + + $cancellationQueue->enqueue($promiseOrValue); + + resolve($promiseOrValue) + ->done($fulfiller, $rejecter); + } + }, $cancellationQueue); +} + +function map(array $promisesOrValues, callable $mapFunc) +{ + if (!$promisesOrValues) { + return resolve([]); + } + + $cancellationQueue = new CancellationQueue(); + + return new Promise(function ($resolve, $reject) use ($promisesOrValues, $mapFunc, $cancellationQueue) { + $toResolve = count($promisesOrValues); + $values = []; + + foreach ($promisesOrValues as $i => $promiseOrValue) { + $cancellationQueue->enqueue($promiseOrValue); + + resolve($promiseOrValue) + ->then($mapFunc) + ->done( + function ($mapped) use ($i, &$values, &$toResolve, $resolve) { + $values[$i] = $mapped; + + if (0 === --$toResolve) { + $resolve($values); + } + }, + $reject + ); + } + }, $cancellationQueue); +} + +function reduce(array $promisesOrValues, callable $reduceFunc, $initialValue = null) +{ + $cancellationQueue = new CancellationQueue(); + + return new Promise(function ($resolve, $reject) use ($promisesOrValues, $reduceFunc, $initialValue, $cancellationQueue) { + $total = count($promisesOrValues); + $i = 0; + + $wrappedReduceFunc = function ($current, $val) use ($reduceFunc, $cancellationQueue, $total, &$i) { + $cancellationQueue->enqueue($val); + + return $current + ->then(function ($c) use ($reduceFunc, $total, &$i, $val) { + return resolve($val) + ->then(function ($value) use ($reduceFunc, $total, &$i, $c) { + return $reduceFunc($c, $value, $i++, $total); + }); + }); + }; + + $cancellationQueue->enqueue($initialValue); + + array_reduce($promisesOrValues, $wrappedReduceFunc, resolve($initialValue)) + ->done($resolve, $reject); + }, $cancellationQueue); +} + +function queue(Queue\QueueInterface $queue = null) +{ + static $globalQueue; + + if ($queue) { + return ($globalQueue = $queue); + } + + if (!$globalQueue) { + $globalQueue = new Queue\SynchronousQueue(); + } + + return $globalQueue; +} + +// Internal functions +function _checkTypehint(callable $callback, $object) +{ + if (!is_object($object)) { + return true; + } + + if (is_array($callback)) { + $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); + } elseif (is_object($callback) && !$callback instanceof \Closure) { + $callbackReflection = new \ReflectionMethod($callback, '__invoke'); + } else { + $callbackReflection = new \ReflectionFunction($callback); + } + + $parameters = $callbackReflection->getParameters(); + + if (!isset($parameters[0])) { + return true; + } + + $expectedException = $parameters[0]; + + if (!$expectedException->getClass()) { + return true; + } + + return $expectedException->getClass()->isInstance($object); +} diff --git a/Framework/lib/Promise/functions_include.php b/Framework/lib/Promise/functions_include.php new file mode 100755 index 0000000..c71decb --- /dev/null +++ b/Framework/lib/Promise/functions_include.php @@ -0,0 +1,5 @@ +getDefaultOptions($request, $headers); + $this->applyMethod($request, $options); + + if (isset($request['client'])) { + $this->applyHandlerOptions($request, $options); + } + + $this->applyHeaders($request, $options); + unset($options['_headers']); + + // Add handler options from the request's configuration options + if (isset($request['client']['curl'])) { + $options = $this->applyCustomCurlOptions( + $request['client']['curl'], + $options + ); + } + + if (!$handle) { + $handle = curl_init(); + } + + $body = $this->getOutputBody($request, $options); + curl_setopt_array($handle, $options); + + return [$handle, &$headers, $body]; + } + + /** + * Creates a response hash from a cURL result. + * + * @param callable $handler Handler that was used. + * @param array $request Request that sent. + * @param array $response Response hash to update. + * @param array $headers Headers received during transfer. + * @param resource $body Body fopen response. + * + * @return array + */ + public static function createResponse( + callable $handler, + array $request, + array $response, + array $headers, + $body + ) { + if (isset($response['transfer_stats']['url'])) { + $response['effective_url'] = $response['transfer_stats']['url']; + } + + if (!empty($headers)) { + $startLine = explode(' ', array_shift($headers), 3); + $headerList = Core::headersFromLines($headers); + $response['headers'] = $headerList; + $response['version'] = isset($startLine[0]) ? substr($startLine[0], 5) : null; + $response['status'] = isset($startLine[1]) ? (int) $startLine[1] : null; + $response['reason'] = isset($startLine[2]) ? $startLine[2] : null; + $response['body'] = $body; + Core::rewindBody($response); + } + + return !empty($response['curl']['errno']) || !isset($response['status']) + ? self::createErrorResponse($handler, $request, $response) + : $response; + } + + private static function createErrorResponse( + callable $handler, + array $request, + array $response + ) { + static $connectionErrors = [ + CURLE_OPERATION_TIMEOUTED => true, + CURLE_COULDNT_RESOLVE_HOST => true, + CURLE_COULDNT_CONNECT => true, + CURLE_SSL_CONNECT_ERROR => true, + CURLE_GOT_NOTHING => true, + ]; + + // Retry when nothing is present or when curl failed to rewind. + if (!isset($response['err_message']) + && (empty($response['curl']['errno']) + || $response['curl']['errno'] == 65) + ) { + return self::retryFailedRewind($handler, $request, $response); + } + + $message = isset($response['err_message']) + ? $response['err_message'] + : sprintf('cURL error %s: %s', + $response['curl']['errno'], + isset($response['curl']['error']) + ? $response['curl']['error'] + : 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html'); + + $error = isset($response['curl']['errno']) + && isset($connectionErrors[$response['curl']['errno']]) + ? new ConnectException($message) + : new RingException($message); + + return $response + [ + 'status' => null, + 'reason' => null, + 'body' => null, + 'headers' => [], + 'error' => $error, + ]; + } + + private function getOutputBody(array $request, array &$options) + { + // Determine where the body of the response (if any) will be streamed. + if (isset($options[CURLOPT_WRITEFUNCTION])) { + return $request['client']['save_to']; + } + + if (isset($options[CURLOPT_FILE])) { + return $options[CURLOPT_FILE]; + } + + if ($request['http_method'] != 'HEAD') { + // Create a default body if one was not provided + return $options[CURLOPT_FILE] = fopen('php://temp', 'w+'); + } + + return null; + } + + private function getDefaultOptions(array $request, array &$headers) + { + $url = Core::url($request); + $startingResponse = false; + + $options = [ + '_headers' => $request['headers'], + CURLOPT_CUSTOMREQUEST => $request['http_method'], + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_HEADER => false, + CURLOPT_CONNECTTIMEOUT => 150, + CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$headers, &$startingResponse) { + $value = trim($h); + if ($value === '') { + $startingResponse = true; + } elseif ($startingResponse) { + $startingResponse = false; + $headers = [$value]; + } else { + $headers[] = $value; + } + return strlen($h); + }, + ]; + + if (isset($request['version'])) { + if ($request['version'] == 2.0) { + $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } else if ($request['version'] == 1.1) { + $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; + } else { + $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; + } + } + + if (defined('CURLOPT_PROTOCOLS')) { + $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + return $options; + } + + private function applyMethod(array $request, array &$options) + { + if (isset($request['body'])) { + $this->applyBody($request, $options); + return; + } + + switch ($request['http_method']) { + case 'PUT': + case 'POST': + // See http://tools.ietf.org/html/rfc7230#section-3.3.2 + if (!Core::hasHeader($request, 'Content-Length')) { + $options[CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; + } + break; + case 'HEAD': + $options[CURLOPT_NOBODY] = true; + unset( + $options[CURLOPT_WRITEFUNCTION], + $options[CURLOPT_READFUNCTION], + $options[CURLOPT_FILE], + $options[CURLOPT_INFILE] + ); + } + } + + private function applyBody(array $request, array &$options) + { + $contentLength = Core::firstHeader($request, 'Content-Length'); + $size = $contentLength !== null ? (int) $contentLength : null; + + // Send the body as a string if the size is less than 1MB OR if the + // [client][curl][body_as_string] request value is set. + if (($size !== null && $size < 1000000) || + isset($request['client']['curl']['body_as_string']) || + is_string($request['body']) + ) { + $options[CURLOPT_POSTFIELDS] = Core::body($request); + // Don't duplicate the Content-Length header + $this->removeHeader('Content-Length', $options); + $this->removeHeader('Transfer-Encoding', $options); + } else { + $options[CURLOPT_UPLOAD] = true; + if ($size !== null) { + // Let cURL handle setting the Content-Length header + $options[CURLOPT_INFILESIZE] = $size; + $this->removeHeader('Content-Length', $options); + } + $this->addStreamingBody($request, $options); + } + + // If the Expect header is not present, prevent curl from adding it + if (!Core::hasHeader($request, 'Expect')) { + $options[CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + // cURL sometimes adds a content-type by default. Prevent this. + if (!Core::hasHeader($request, 'Content-Type')) { + $options[CURLOPT_HTTPHEADER][] = 'Content-Type:'; + } + } + + private function addStreamingBody(array $request, array &$options) + { + $body = $request['body']; + + if ($body instanceof StreamInterface) { + $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { + return (string) $body->read($length); + }; + if (!isset($options[CURLOPT_INFILESIZE])) { + if ($size = $body->getSize()) { + $options[CURLOPT_INFILESIZE] = $size; + } + } + } elseif (is_resource($body)) { + $options[CURLOPT_INFILE] = $body; + } elseif ($body instanceof \Iterator) { + $buf = ''; + $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, &$buf) { + if ($body->valid()) { + $buf .= $body->current(); + $body->next(); + } + $result = (string) substr($buf, 0, $length); + $buf = substr($buf, $length); + return $result; + }; + } else { + throw new \InvalidArgumentException('Invalid request body provided'); + } + } + + private function applyHeaders(array $request, array &$options) + { + foreach ($options['_headers'] as $name => $values) { + foreach ($values as $value) { + $options[CURLOPT_HTTPHEADER][] = "$name: $value"; + } + } + + // Remove the Accept header if one was not set + if (!Core::hasHeader($request, 'Accept')) { + $options[CURLOPT_HTTPHEADER][] = 'Accept:'; + } + } + + /** + * Takes an array of curl options specified in the 'curl' option of a + * request's configuration array and maps them to CURLOPT_* options. + * + * This method is only called when a request has a 'curl' config setting. + * + * @param array $config Configuration array of custom curl option + * @param array $options Array of existing curl options + * + * @return array Returns a new array of curl options + */ + private function applyCustomCurlOptions(array $config, array $options) + { + $curlOptions = []; + foreach ($config as $key => $value) { + if (is_int($key)) { + $curlOptions[$key] = $value; + } + } + + return $curlOptions + $options; + } + + /** + * Remove a header from the options array. + * + * @param string $name Case-insensitive header to remove + * @param array $options Array of options to modify + */ + private function removeHeader($name, array &$options) + { + foreach (array_keys($options['_headers']) as $key) { + if (!strcasecmp($key, $name)) { + unset($options['_headers'][$key]); + return; + } + } + } + + /** + * Applies an array of request client options to a the options array. + * + * This method uses a large switch rather than double-dispatch to save on + * high overhead of calling functions in PHP. + */ + private function applyHandlerOptions(array $request, array &$options) + { + foreach ($request['client'] as $key => $value) { + switch ($key) { + // Violating PSR-4 to provide more room. + case 'verify': + + if ($value === false) { + unset($options[CURLOPT_CAINFO]); + $options[CURLOPT_SSL_VERIFYHOST] = 0; + $options[CURLOPT_SSL_VERIFYPEER] = false; + continue; + } + + $options[CURLOPT_SSL_VERIFYHOST] = 2; + $options[CURLOPT_SSL_VERIFYPEER] = true; + + if (is_string($value)) { + $options[CURLOPT_CAINFO] = $value; + if (!file_exists($value)) { + throw new \InvalidArgumentException( + "SSL CA bundle not found: $value" + ); + } + } + break; + + case 'decode_content': + + if ($value === false) { + continue; + } + + $accept = Core::firstHeader($request, 'Accept-Encoding'); + if ($accept) { + $options[CURLOPT_ENCODING] = $accept; + } else { + $options[CURLOPT_ENCODING] = ''; + // Don't let curl send the header over the wire + $options[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; + } + break; + + case 'save_to': + + if (is_string($value)) { + if (!is_dir(dirname($value))) { + throw new \RuntimeException(sprintf( + 'Directory %s does not exist for save_to value of %s', + dirname($value), + $value + )); + } + $value = new LazyOpenStream($value, 'w+'); + } + + if ($value instanceof StreamInterface) { + $options[CURLOPT_WRITEFUNCTION] = + function ($ch, $write) use ($value) { + return $value->write($write); + }; + } elseif (is_resource($value)) { + $options[CURLOPT_FILE] = $value; + } else { + throw new \InvalidArgumentException('save_to must be a ' + . 'GuzzleHttp\Stream\StreamInterface or resource'); + } + break; + + case 'timeout': + + if (defined('CURLOPT_TIMEOUT_MS')) { + $options[CURLOPT_TIMEOUT_MS] = $value * 1000; + } else { + $options[CURLOPT_TIMEOUT] = $value; + } + break; + + case 'connect_timeout': + + if (defined('CURLOPT_CONNECTTIMEOUT_MS')) { + $options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000; + } else { + $options[CURLOPT_CONNECTTIMEOUT] = $value; + } + break; + + case 'proxy': + + if (!is_array($value)) { + $options[CURLOPT_PROXY] = $value; + } elseif (isset($request['scheme'])) { + $scheme = $request['scheme']; + if (isset($value[$scheme])) { + $options[CURLOPT_PROXY] = $value[$scheme]; + } + } + break; + + case 'cert': + + if (is_array($value)) { + $options[CURLOPT_SSLCERTPASSWD] = $value[1]; + $value = $value[0]; + } + + if (!file_exists($value)) { + throw new \InvalidArgumentException( + "SSL certificate not found: {$value}" + ); + } + + $options[CURLOPT_SSLCERT] = $value; + break; + + case 'ssl_key': + + if (is_array($value)) { + $options[CURLOPT_SSLKEYPASSWD] = $value[1]; + $value = $value[0]; + } + + if (!file_exists($value)) { + throw new \InvalidArgumentException( + "SSL private key not found: {$value}" + ); + } + + $options[CURLOPT_SSLKEY] = $value; + break; + + case 'progress': + + if (!is_callable($value)) { + throw new \InvalidArgumentException( + 'progress client option must be callable' + ); + } + + $options[CURLOPT_NOPROGRESS] = false; + $options[CURLOPT_PROGRESSFUNCTION] = + function () use ($value) { + $args = func_get_args(); + // PHP 5.5 pushed the handle onto the start of the args + if (is_resource($args[0])) { + array_shift($args); + } + call_user_func_array($value, $args); + }; + break; + + case 'debug': + + if ($value) { + $options[CURLOPT_STDERR] = Core::getDebugResource($value); + $options[CURLOPT_VERBOSE] = true; + } + break; + } + } + } + + /** + * This function ensures that a response was set on a transaction. If one + * was not set, then the request is retried if possible. This error + * typically means you are sending a payload, curl encountered a + * "Connection died, retrying a fresh connect" error, tried to rewind the + * stream, and then encountered a "necessary data rewind wasn't possible" + * error, causing the request to be sent through curl_multi_info_read() + * without an error status. + */ + private static function retryFailedRewind( + callable $handler, + array $request, + array $response + ) { + // If there is no body, then there is some other kind of issue. This + // is weird and should probably never happen. + if (!isset($request['body'])) { + $response['err_message'] = 'No response was received for a request ' + . 'with no body. This could mean that you are saturating your ' + . 'network.'; + return self::createErrorResponse($handler, $request, $response); + } + + if (!Core::rewindBody($request)) { + $response['err_message'] = 'The connection unexpectedly failed ' + . 'without providing an error. The request would have been ' + . 'retried, but attempting to rewind the request body failed.'; + return self::createErrorResponse($handler, $request, $response); + } + + // Retry no more than 3 times before giving up. + if (!isset($request['curl']['retries'])) { + $request['curl']['retries'] = 1; + } elseif ($request['curl']['retries'] == 2) { + $response['err_message'] = 'The cURL request was retried 3 times ' + . 'and did no succeed. cURL was unable to rewind the body of ' + . 'the request and subsequent retries resulted in the same ' + . 'error. Turn on the debug option to see what went wrong. ' + . 'See https://bugs.php.net/bug.php?id=47204 for more information.'; + return self::createErrorResponse($handler, $request, $response); + } else { + $request['curl']['retries']++; + } + + return $handler($request); + } +} diff --git a/Framework/lib/Ring/Client/CurlHandler.php b/Framework/lib/Ring/Client/CurlHandler.php new file mode 100755 index 0000000..e00aa4e --- /dev/null +++ b/Framework/lib/Ring/Client/CurlHandler.php @@ -0,0 +1,135 @@ +handles = $this->ownedHandles = []; + $this->factory = isset($options['handle_factory']) + ? $options['handle_factory'] + : new CurlFactory(); + $this->maxHandles = isset($options['max_handles']) + ? $options['max_handles'] + : 5; + } + + public function __destruct() + { + foreach ($this->handles as $handle) { + if (is_resource($handle)) { + curl_close($handle); + } + } + } + + /** + * @param array $request + * + * @return CompletedFutureArray + */ + public function __invoke(array $request) + { + return new CompletedFutureArray( + $this->_invokeAsArray($request) + ); + } + + /** + * @internal + * + * @param array $request + * + * @return array + */ + public function _invokeAsArray(array $request) + { + $factory = $this->factory; + + // Ensure headers are by reference. They're updated elsewhere. + $result = $factory($request, $this->checkoutEasyHandle()); + $h = $result[0]; + $hd =& $result[1]; + $bd = $result[2]; + Core::doSleep($request); + curl_exec($h); + $response = ['transfer_stats' => curl_getinfo($h)]; + $response['curl']['error'] = curl_error($h); + $response['curl']['errno'] = curl_errno($h); + $response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']); + $this->releaseEasyHandle($h); + + return CurlFactory::createResponse([$this, '_invokeAsArray'], $request, $response, $hd, $bd); + } + + private function checkoutEasyHandle() + { + // Find an unused handle in the cache + if (false !== ($key = array_search(false, $this->ownedHandles, true))) { + $this->ownedHandles[$key] = true; + return $this->handles[$key]; + } + + // Add a new handle + $handle = curl_init(); + $id = (int) $handle; + $this->handles[$id] = $handle; + $this->ownedHandles[$id] = true; + + return $handle; + } + + private function releaseEasyHandle($handle) + { + $id = (int) $handle; + if (count($this->ownedHandles) > $this->maxHandles) { + curl_close($this->handles[$id]); + unset($this->handles[$id], $this->ownedHandles[$id]); + } else { + // curl_reset doesn't clear these out for some reason + static $unsetValues = [ + CURLOPT_HEADERFUNCTION => null, + CURLOPT_WRITEFUNCTION => null, + CURLOPT_READFUNCTION => null, + CURLOPT_PROGRESSFUNCTION => null, + ]; + curl_setopt_array($handle, $unsetValues); + curl_reset($handle); + $this->ownedHandles[$id] = false; + } + } +} diff --git a/Framework/lib/Ring/Client/CurlMultiHandler.php b/Framework/lib/Ring/Client/CurlMultiHandler.php new file mode 100755 index 0000000..f84cf19 --- /dev/null +++ b/Framework/lib/Ring/Client/CurlMultiHandler.php @@ -0,0 +1,248 @@ +_mh = $options['mh']; + } + $this->factory = isset($options['handle_factory']) + ? $options['handle_factory'] : new CurlFactory(); + $this->selectTimeout = isset($options['select_timeout']) + ? $options['select_timeout'] : 1; + $this->maxHandles = isset($options['max_handles']) + ? $options['max_handles'] : 100; + } + + public function __get($name) + { + if ($name === '_mh') { + return $this->_mh = curl_multi_init(); + } + + throw new \BadMethodCallException(); + } + + public function __destruct() + { + // Finish any open connections before terminating the script. + if ($this->handles) { + $this->execute(); + } + + if (isset($this->_mh)) { + curl_multi_close($this->_mh); + unset($this->_mh); + } + } + + public function __invoke(array $request) + { + $factory = $this->factory; + $result = $factory($request); + $entry = [ + 'request' => $request, + 'response' => [], + 'handle' => $result[0], + 'headers' => &$result[1], + 'body' => $result[2], + 'deferred' => new Deferred(), + ]; + + $id = (int) $result[0]; + + $future = new FutureArray( + $entry['deferred']->promise(), + [$this, 'execute'], + function () use ($id) { + return $this->cancel($id); + } + ); + + $this->addRequest($entry); + + // Transfer outstanding requests if there are too many open handles. + if (count($this->handles) >= $this->maxHandles) { + $this->execute(); + } + + return $future; + } + + /** + * Runs until all outstanding connections have completed. + */ + public function execute() + { + do { + + if ($this->active && + curl_multi_select($this->_mh, $this->selectTimeout) === -1 + ) { + // Perform a usleep if a select returns -1. + // See: https://bugs.php.net/bug.php?id=61141 + usleep(250); + } + + // Add any delayed futures if needed. + if ($this->delays) { + $this->addDelays(); + } + + do { + $mrc = curl_multi_exec($this->_mh, $this->active); + } while ($mrc === CURLM_CALL_MULTI_PERFORM); + + $this->processMessages(); + + // If there are delays but no transfers, then sleep for a bit. + if (!$this->active && $this->delays) { + usleep(500); + } + + } while ($this->active || $this->handles); + } + + private function addRequest(array &$entry) + { + $id = (int) $entry['handle']; + $this->handles[$id] = $entry; + + // If the request is a delay, then add the reques to the curl multi + // pool only after the specified delay. + if (isset($entry['request']['client']['delay'])) { + $this->delays[$id] = microtime(true) + ($entry['request']['client']['delay'] / 1000); + } elseif (empty($entry['request']['future'])) { + curl_multi_add_handle($this->_mh, $entry['handle']); + } else { + curl_multi_add_handle($this->_mh, $entry['handle']); + // "lazy" futures are only sent once the pool has many requests. + if ($entry['request']['future'] !== 'lazy') { + do { + $mrc = curl_multi_exec($this->_mh, $this->active); + } while ($mrc === CURLM_CALL_MULTI_PERFORM); + $this->processMessages(); + } + } + } + + private function removeProcessed($id) + { + if (isset($this->handles[$id])) { + curl_multi_remove_handle( + $this->_mh, + $this->handles[$id]['handle'] + ); + curl_close($this->handles[$id]['handle']); + unset($this->handles[$id], $this->delays[$id]); + } + } + + /** + * Cancels a handle from sending and removes references to it. + * + * @param int $id Handle ID to cancel and remove. + * + * @return bool True on success, false on failure. + */ + private function cancel($id) + { + // Cannot cancel if it has been processed. + if (!isset($this->handles[$id])) { + return false; + } + + $handle = $this->handles[$id]['handle']; + unset($this->delays[$id], $this->handles[$id]); + curl_multi_remove_handle($this->_mh, $handle); + curl_close($handle); + + return true; + } + + private function addDelays() + { + $currentTime = microtime(true); + + foreach ($this->delays as $id => $delay) { + if ($currentTime >= $delay) { + unset($this->delays[$id]); + curl_multi_add_handle( + $this->_mh, + $this->handles[$id]['handle'] + ); + } + } + } + + private function processMessages() + { + while ($done = curl_multi_info_read($this->_mh)) { + $id = (int) $done['handle']; + + if (!isset($this->handles[$id])) { + // Probably was cancelled. + continue; + } + + $entry = $this->handles[$id]; + $entry['response']['transfer_stats'] = curl_getinfo($done['handle']); + + if ($done['result'] !== CURLM_OK) { + $entry['response']['curl']['errno'] = $done['result']; + $entry['response']['curl']['error'] = curl_error($done['handle']); + } + + $result = CurlFactory::createResponse( + $this, + $entry['request'], + $entry['response'], + $entry['headers'], + $entry['body'] + ); + + $this->removeProcessed($id); + $entry['deferred']->resolve($result); + } + } +} diff --git a/Framework/lib/Ring/Client/Middleware.php b/Framework/lib/Ring/Client/Middleware.php new file mode 100755 index 0000000..6fa7318 --- /dev/null +++ b/Framework/lib/Ring/Client/Middleware.php @@ -0,0 +1,58 @@ +result = $result; + } + + public function __invoke(array $request) + { + Core::doSleep($request); + $response = is_callable($this->result) + ? call_user_func($this->result, $request) + : $this->result; + + if (is_array($response)) { + $response = new CompletedFutureArray($response + [ + 'status' => null, + 'body' => null, + 'headers' => [], + 'reason' => null, + 'effective_url' => null, + ]); + } elseif (!$response instanceof FutureArrayInterface) { + throw new \InvalidArgumentException( + 'Response must be an array or FutureArrayInterface. Found ' + . Core::describeType($request) + ); + } + + return $response; + } +} diff --git a/Framework/lib/Ring/Client/StreamHandler.php b/Framework/lib/Ring/Client/StreamHandler.php new file mode 100755 index 0000000..4bacec1 --- /dev/null +++ b/Framework/lib/Ring/Client/StreamHandler.php @@ -0,0 +1,414 @@ +options = $options; + } + + public function __invoke(array $request) + { + $url = Core::url($request); + Core::doSleep($request); + + try { + // Does not support the expect header. + $request = Core::removeHeader($request, 'Expect'); + $stream = $this->createStream($url, $request); + return $this->createResponse($request, $url, $stream); + } catch (RingException $e) { + return $this->createErrorResponse($url, $e); + } + } + + private function createResponse(array $request, $url, $stream) + { + $hdrs = $this->lastHeaders; + $this->lastHeaders = null; + $parts = explode(' ', array_shift($hdrs), 3); + $response = [ + 'version' => substr($parts[0], 5), + 'status' => $parts[1], + 'reason' => isset($parts[2]) ? $parts[2] : null, + 'headers' => Core::headersFromLines($hdrs), + 'effective_url' => $url, + ]; + + $stream = $this->checkDecode($request, $response, $stream); + + // If not streaming, then drain the response into a stream. + if (empty($request['client']['stream'])) { + $dest = isset($request['client']['save_to']) + ? $request['client']['save_to'] + : fopen('php://temp', 'r+'); + $stream = $this->drain($stream, $dest); + } + + $response['body'] = $stream; + + return new CompletedFutureArray($response); + } + + private function checkDecode(array $request, array $response, $stream) + { + // Automatically decode responses when instructed. + if (!empty($request['client']['decode_content'])) { + switch (Core::firstHeader($response, 'Content-Encoding', true)) { + case 'gzip': + case 'deflate': + $stream = new InflateStream(Stream::factory($stream)); + break; + } + } + + return $stream; + } + + /** + * Drains the stream into the "save_to" client option. + * + * @param resource $stream + * @param string|resource|StreamInterface $dest + * + * @return Stream + * @throws \RuntimeException when the save_to option is invalid. + */ + private function drain($stream, $dest) + { + if (is_resource($stream)) { + if (!is_resource($dest)) { + $stream = Stream::factory($stream); + } else { + stream_copy_to_stream($stream, $dest); + fclose($stream); + rewind($dest); + return $dest; + } + } + + // Stream the response into the destination stream + $dest = is_string($dest) + ? new Stream(Utils::open($dest, 'r+')) + : Stream::factory($dest); + + Utils::copyToStream($stream, $dest); + $dest->seek(0); + $stream->close(); + + return $dest; + } + + /** + * Creates an error response for the given stream. + * + * @param string $url + * @param RingException $e + * + * @return array + */ + private function createErrorResponse($url, RingException $e) + { + // Determine if the error was a networking error. + $message = $e->getMessage(); + + // This list can probably get more comprehensive. + if (strpos($message, 'getaddrinfo') // DNS lookup failed + || strpos($message, 'Connection refused') + ) { + $e = new ConnectException($e->getMessage(), 0, $e); + } + + return new CompletedFutureArray([ + 'status' => null, + 'body' => null, + 'headers' => [], + 'effective_url' => $url, + 'error' => $e + ]); + } + + /** + * Create a resource and check to ensure it was created successfully + * + * @param callable $callback Callable that returns stream resource + * + * @return resource + * @throws \RuntimeException on error + */ + private function createResource(callable $callback) + { + $errors = null; + set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { + $errors[] = [ + 'message' => $msg, + 'file' => $file, + 'line' => $line + ]; + return true; + }); + + $resource = $callback(); + restore_error_handler(); + + if (!$resource) { + $message = 'Error creating resource: '; + foreach ($errors as $err) { + foreach ($err as $key => $value) { + $message .= "[$key] $value" . PHP_EOL; + } + } + throw new RingException(trim($message)); + } + + return $resource; + } + + private function createStream($url, array $request) + { + static $methods; + if (!$methods) { + $methods = array_flip(get_class_methods(__CLASS__)); + } + + // HTTP/1.1 streams using the PHP stream wrapper require a + // Connection: close header + if ((!isset($request['version']) || $request['version'] == '1.1') + && !Core::hasHeader($request, 'Connection') + ) { + $request['headers']['Connection'] = ['close']; + } + + // Ensure SSL is verified by default + if (!isset($request['client']['verify'])) { + $request['client']['verify'] = true; + } + + $params = []; + $options = $this->getDefaultOptions($request); + + if (isset($request['client'])) { + foreach ($request['client'] as $key => $value) { + $method = "add_{$key}"; + if (isset($methods[$method])) { + $this->{$method}($request, $options, $value, $params); + } + } + } + + return $this->createStreamResource( + $url, + $request, + $options, + $this->createContext($request, $options, $params) + ); + } + + private function getDefaultOptions(array $request) + { + $headers = ""; + foreach ($request['headers'] as $name => $value) { + foreach ((array) $value as $val) { + $headers .= "$name: $val\r\n"; + } + } + + $context = [ + 'http' => [ + 'method' => $request['http_method'], + 'header' => $headers, + 'protocol_version' => isset($request['version']) ? $request['version'] : 1.1, + 'ignore_errors' => true, + 'follow_location' => 0, + ], + ]; + + $body = Core::body($request); + if (isset($body)) { + $context['http']['content'] = $body; + // Prevent the HTTP handler from adding a Content-Type header. + if (!Core::hasHeader($request, 'Content-Type')) { + $context['http']['header'] .= "Content-Type:\r\n"; + } + } + + $context['http']['header'] = rtrim($context['http']['header']); + + return $context; + } + + private function add_proxy(array $request, &$options, $value, &$params) + { + if (!is_array($value)) { + $options['http']['proxy'] = $value; + } else { + $scheme = isset($request['scheme']) ? $request['scheme'] : 'http'; + if (isset($value[$scheme])) { + $options['http']['proxy'] = $value[$scheme]; + } + } + } + + private function add_timeout(array $request, &$options, $value, &$params) + { + $options['http']['timeout'] = $value; + } + + private function add_verify(array $request, &$options, $value, &$params) + { + if ($value === true) { + // PHP 5.6 or greater will find the system cert by default. When + // < 5.6, use the Guzzle bundled cacert. + if (PHP_VERSION_ID < 50600) { + $options['ssl']['cafile'] = ClientUtils::getDefaultCaBundle(); + } + } elseif (is_string($value)) { + $options['ssl']['cafile'] = $value; + if (!file_exists($value)) { + throw new RingException("SSL CA bundle not found: $value"); + } + } elseif ($value === false) { + $options['ssl']['verify_peer'] = false; + $options['ssl']['allow_self_signed'] = true; + return; + } else { + throw new RingException('Invalid verify request option'); + } + + $options['ssl']['verify_peer'] = true; + $options['ssl']['allow_self_signed'] = false; + } + + private function add_cert(array $request, &$options, $value, &$params) + { + if (is_array($value)) { + $options['ssl']['passphrase'] = $value[1]; + $value = $value[0]; + } + + if (!file_exists($value)) { + throw new RingException("SSL certificate not found: {$value}"); + } + + $options['ssl']['local_cert'] = $value; + } + + private function add_progress(array $request, &$options, $value, &$params) + { + $fn = function ($code, $_1, $_2, $_3, $transferred, $total) use ($value) { + if ($code == STREAM_NOTIFY_PROGRESS) { + $value($total, $transferred, null, null); + } + }; + + // Wrap the existing function if needed. + $params['notification'] = isset($params['notification']) + ? Core::callArray([$params['notification'], $fn]) + : $fn; + } + + private function add_debug(array $request, &$options, $value, &$params) + { + if ($value === false) { + return; + } + + static $map = [ + STREAM_NOTIFY_CONNECT => 'CONNECT', + STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', + STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', + STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', + STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', + STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', + STREAM_NOTIFY_PROGRESS => 'PROGRESS', + STREAM_NOTIFY_FAILURE => 'FAILURE', + STREAM_NOTIFY_COMPLETED => 'COMPLETED', + STREAM_NOTIFY_RESOLVE => 'RESOLVE', + ]; + + static $args = ['severity', 'message', 'message_code', + 'bytes_transferred', 'bytes_max']; + + $value = Core::getDebugResource($value); + $ident = $request['http_method'] . ' ' . Core::url($request); + $fn = function () use ($ident, $value, $map, $args) { + $passed = func_get_args(); + $code = array_shift($passed); + fprintf($value, '<%s> [%s] ', $ident, $map[$code]); + foreach (array_filter($passed) as $i => $v) { + fwrite($value, $args[$i] . ': "' . $v . '" '); + } + fwrite($value, "\n"); + }; + + // Wrap the existing function if needed. + $params['notification'] = isset($params['notification']) + ? Core::callArray([$params['notification'], $fn]) + : $fn; + } + + private function applyCustomOptions(array $request, array &$options) + { + if (!isset($request['client']['stream_context'])) { + return; + } + + if (!is_array($request['client']['stream_context'])) { + throw new RingException('stream_context must be an array'); + } + + $options = array_replace_recursive( + $options, + $request['client']['stream_context'] + ); + } + + private function createContext(array $request, array $options, array $params) + { + $this->applyCustomOptions($request, $options); + return $this->createResource( + function () use ($request, $options, $params) { + return stream_context_create($options, $params); + }, + $request, + $options + ); + } + + private function createStreamResource( + $url, + array $request, + array $options, + $context + ) { + return $this->createResource( + function () use ($url, $context) { + if (false === strpos($url, 'http')) { + trigger_error("URL is invalid: {$url}", E_USER_WARNING); + return null; + } + $resource = fopen($url, 'r', null, $context); + $this->lastHeaders = $http_response_header; + return $resource; + }, + $request, + $options + ); + } +} diff --git a/Framework/lib/Ring/Core.php b/Framework/lib/Ring/Core.php new file mode 100755 index 0000000..dd7d1a0 --- /dev/null +++ b/Framework/lib/Ring/Core.php @@ -0,0 +1,364 @@ + $value) { + if (!strcasecmp($name, $header)) { + $result = array_merge($result, $value); + } + } + } + + return $result; + } + + /** + * Gets a header value from a message as a string or null + * + * This method searches through the "headers" key of a message for a header + * using a case-insensitive search. The lines of the header are imploded + * using commas into a single string return value. + * + * @param array $message Request or response hash. + * @param string $header Header to retrieve + * + * @return string|null Returns the header string if found, or null if not. + */ + public static function header($message, $header) + { + $match = self::headerLines($message, $header); + return $match ? implode(', ', $match) : null; + } + + /** + * Returns the first header value from a message as a string or null. If + * a header line contains multiple values separated by a comma, then this + * function will return the first value in the list. + * + * @param array $message Request or response hash. + * @param string $header Header to retrieve + * + * @return string|null Returns the value as a string if found. + */ + public static function firstHeader($message, $header) + { + if (!empty($message['headers'])) { + foreach ($message['headers'] as $name => $value) { + if (!strcasecmp($name, $header)) { + // Return the match itself if it is a single value. + $pos = strpos($value[0], ','); + return $pos ? substr($value[0], 0, $pos) : $value[0]; + } + } + } + + return null; + } + + /** + * Returns true if a message has the provided case-insensitive header. + * + * @param array $message Request or response hash. + * @param string $header Header to check + * + * @return bool + */ + public static function hasHeader($message, $header) + { + if (!empty($message['headers'])) { + foreach ($message['headers'] as $name => $value) { + if (!strcasecmp($name, $header)) { + return true; + } + } + } + + return false; + } + + /** + * Parses an array of header lines into an associative array of headers. + * + * @param array $lines Header lines array of strings in the following + * format: "Name: Value" + * @return array + */ + public static function headersFromLines($lines) + { + $headers = []; + + foreach ($lines as $line) { + $parts = explode(':', $line, 2); + $headers[trim($parts[0])][] = isset($parts[1]) + ? trim($parts[1]) + : null; + } + + return $headers; + } + + /** + * Removes a header from a message using a case-insensitive comparison. + * + * @param array $message Message that contains 'headers' + * @param string $header Header to remove + * + * @return array + */ + public static function removeHeader(array $message, $header) + { + if (isset($message['headers'])) { + foreach (array_keys($message['headers']) as $key) { + if (!strcasecmp($header, $key)) { + unset($message['headers'][$key]); + } + } + } + + return $message; + } + + /** + * Replaces any existing case insensitive headers with the given value. + * + * @param array $message Message that contains 'headers' + * @param string $header Header to set. + * @param array $value Value to set. + * + * @return array + */ + public static function setHeader(array $message, $header, array $value) + { + $message = self::removeHeader($message, $header); + $message['headers'][$header] = $value; + + return $message; + } + + /** + * Creates a URL string from a request. + * + * If the "url" key is present on the request, it is returned, otherwise + * the url is built up based on the scheme, host, uri, and query_string + * request values. + * + * @param array $request Request to get the URL from + * + * @return string Returns the request URL as a string. + * @throws \InvalidArgumentException if no Host header is present. + */ + public static function url(array $request) + { + if (isset($request['url'])) { + return $request['url']; + } + + $uri = (isset($request['scheme']) + ? $request['scheme'] : 'http') . '://'; + + if ($host = self::header($request, 'host')) { + $uri .= $host; + } else { + throw new \InvalidArgumentException('No Host header was provided'); + } + + if (isset($request['uri'])) { + $uri .= $request['uri']; + } + + if (isset($request['query_string'])) { + $uri .= '?' . $request['query_string']; + } + + return $uri; + } + + /** + * Reads the body of a message into a string. + * + * @param array|FutureArrayInterface $message Array containing a "body" key + * + * @return null|string Returns the body as a string or null if not set. + * @throws \InvalidArgumentException if a request body is invalid. + */ + public static function body($message) + { + if (!isset($message['body'])) { + return null; + } + + if ($message['body'] instanceof StreamInterface) { + return (string) $message['body']; + } + + switch (gettype($message['body'])) { + case 'string': + return $message['body']; + case 'resource': + return stream_get_contents($message['body']); + case 'object': + if ($message['body'] instanceof \Iterator) { + return implode('', iterator_to_array($message['body'])); + } elseif (method_exists($message['body'], '__toString')) { + return (string) $message['body']; + } + default: + throw new \InvalidArgumentException('Invalid request body: ' + . self::describeType($message['body'])); + } + } + + /** + * Rewind the body of the provided message if possible. + * + * @param array $message Message that contains a 'body' field. + * + * @return bool Returns true on success, false on failure + */ + public static function rewindBody($message) + { + if ($message['body'] instanceof StreamInterface) { + return $message['body']->seek(0); + } + + if ($message['body'] instanceof \Generator) { + return false; + } + + if ($message['body'] instanceof \Iterator) { + $message['body']->rewind(); + return true; + } + + if (is_resource($message['body'])) { + return rewind($message['body']); + } + + return is_string($message['body']) + || (is_object($message['body']) + && method_exists($message['body'], '__toString')); + } + + /** + * Debug function used to describe the provided value type and class. + * + * @param mixed $input + * + * @return string Returns a string containing the type of the variable and + * if a class is provided, the class name. + */ + public static function describeType($input) + { + switch (gettype($input)) { + case 'object': + return 'object(' . get_class($input) . ')'; + case 'array': + return 'array(' . count($input) . ')'; + default: + ob_start(); + var_dump($input); + // normalize float vs double + return str_replace('double(', 'float(', rtrim(ob_get_clean())); + } + } + + /** + * Sleep for the specified amount of time specified in the request's + * ['client']['delay'] option if present. + * + * This function should only be used when a non-blocking sleep is not + * possible. + * + * @param array $request Request to sleep + */ + public static function doSleep(array $request) + { + if (isset($request['client']['delay'])) { + usleep($request['client']['delay'] * 1000); + } + } + + /** + * Returns a proxied future that modifies the dereferenced value of another + * future using a promise. + * + * @param FutureArrayInterface $future Future to wrap with a new future + * @param callable $onFulfilled Invoked when the future fulfilled + * @param callable $onRejected Invoked when the future rejected + * @param callable $onProgress Invoked when the future progresses + * + * @return FutureArray + */ + public static function proxy( + FutureArrayInterface $future, + callable $onFulfilled = null, + callable $onRejected = null, + callable $onProgress = null + ) { + return new FutureArray( + $future->then($onFulfilled, $onRejected, $onProgress), + [$future, 'wait'], + [$future, 'cancel'] + ); + } + + /** + * Returns a debug stream based on the provided variable. + * + * @param mixed $value Optional value + * + * @return resource + */ + public static function getDebugResource($value = null) + { + if (is_resource($value)) { + return $value; + } elseif (defined('STDOUT')) { + return STDOUT; + } else { + return fopen('php://output', 'w'); + } + } +} diff --git a/Framework/lib/Ring/Exception/CancelledException.php b/Framework/lib/Ring/Exception/CancelledException.php new file mode 100755 index 0000000..95b353a --- /dev/null +++ b/Framework/lib/Ring/Exception/CancelledException.php @@ -0,0 +1,7 @@ +wrappedPromise = $promise; + $this->waitfn = $wait; + $this->cancelfn = $cancel; + } + + public function wait() + { + if (!$this->isRealized) { + $this->addShadow(); + if (!$this->isRealized && $this->waitfn) { + $this->invokeWait(); + } + if (!$this->isRealized) { + $this->error = new RingException('Waiting did not resolve future'); + } + } + + if ($this->error) { + throw $this->error; + } + + return $this->result; + } + + public function promise() + { + return $this->wrappedPromise; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null, + callable $onProgress = null + ) { + return $this->wrappedPromise->then($onFulfilled, $onRejected, $onProgress); + } + + public function cancel() + { + if (!$this->isRealized) { + $cancelfn = $this->cancelfn; + $this->waitfn = $this->cancelfn = null; + $this->isRealized = true; + $this->error = new CancelledFutureAccessException(); + if ($cancelfn) { + $cancelfn($this); + } + } + } + + private function addShadow() + { + // Get the result and error when the promise is resolved. Note that + // calling this function might trigger the resolution immediately. + $this->wrappedPromise->then( + function ($value) { + $this->isRealized = true; + $this->result = $value; + $this->waitfn = $this->cancelfn = null; + }, + function ($error) { + $this->isRealized = true; + $this->error = $error; + $this->waitfn = $this->cancelfn = null; + } + ); + } + + private function invokeWait() + { + try { + $wait = $this->waitfn; + $this->waitfn = null; + $wait(); + } catch (\Exception $e) { + // Defer can throw to reject. + $this->error = $e; + $this->isRealized = true; + } + } +} diff --git a/Framework/lib/Ring/Future/CompletedFutureArray.php b/Framework/lib/Ring/Future/CompletedFutureArray.php new file mode 100755 index 0000000..0a90c93 --- /dev/null +++ b/Framework/lib/Ring/Future/CompletedFutureArray.php @@ -0,0 +1,43 @@ +result[$offset]); + } + + public function offsetGet($offset) + { + return $this->result[$offset]; + } + + public function offsetSet($offset, $value) + { + $this->result[$offset] = $value; + } + + public function offsetUnset($offset) + { + unset($this->result[$offset]); + } + + public function count() + { + return count($this->result); + } + + public function getIterator() + { + return new \ArrayIterator($this->result); + } +} diff --git a/Framework/lib/Ring/Future/CompletedFutureValue.php b/Framework/lib/Ring/Future/CompletedFutureValue.php new file mode 100755 index 0000000..0d25af7 --- /dev/null +++ b/Framework/lib/Ring/Future/CompletedFutureValue.php @@ -0,0 +1,57 @@ +result = $result; + $this->error = $e; + } + + public function wait() + { + if ($this->error) { + throw $this->error; + } + + return $this->result; + } + + public function cancel() {} + + public function promise() + { + if (!$this->cachedPromise) { + $this->cachedPromise = $this->error + ? new RejectedPromise($this->error) + : new FulfilledPromise($this->result); + } + + return $this->cachedPromise; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null, + callable $onProgress = null + ) { + return $this->promise()->then($onFulfilled, $onRejected, $onProgress); + } +} diff --git a/Framework/lib/Ring/Future/FutureArray.php b/Framework/lib/Ring/Future/FutureArray.php new file mode 100755 index 0000000..3d64c96 --- /dev/null +++ b/Framework/lib/Ring/Future/FutureArray.php @@ -0,0 +1,40 @@ +_value[$offset]); + } + + public function offsetGet($offset) + { + return $this->_value[$offset]; + } + + public function offsetSet($offset, $value) + { + $this->_value[$offset] = $value; + } + + public function offsetUnset($offset) + { + unset($this->_value[$offset]); + } + + public function count() + { + return count($this->_value); + } + + public function getIterator() + { + return new \ArrayIterator($this->_value); + } +} diff --git a/Framework/lib/Ring/Future/FutureArrayInterface.php b/Framework/lib/Ring/Future/FutureArrayInterface.php new file mode 100755 index 0000000..58f5f73 --- /dev/null +++ b/Framework/lib/Ring/Future/FutureArrayInterface.php @@ -0,0 +1,11 @@ +_value = $this->wait(); + } +} diff --git a/Framework/lib/S3Helper b/Framework/lib/S3Helper new file mode 160000 index 0000000..207c261 --- /dev/null +++ b/Framework/lib/S3Helper @@ -0,0 +1 @@ +Subproject commit 207c2613df703756213184f9a0a3a254574792b8 diff --git a/Framework/lib/Streams/AppendStream.php b/Framework/lib/Streams/AppendStream.php new file mode 100755 index 0000000..94bda71 --- /dev/null +++ b/Framework/lib/Streams/AppendStream.php @@ -0,0 +1,220 @@ +addStream($stream); + } + } + + public function __toString() + { + try { + $this->seek(0); + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Add a stream to the AppendStream + * + * @param StreamInterface $stream Stream to append. Must be readable. + * + * @throws \InvalidArgumentException if the stream is not readable + */ + public function addStream(StreamInterface $stream) + { + if (!$stream->isReadable()) { + throw new \InvalidArgumentException('Each stream must be readable'); + } + + // The stream is only seekable if all streams are seekable + if (!$stream->isSeekable()) { + $this->seekable = false; + } + + $this->streams[] = $stream; + } + + public function getContents() + { + return Utils::copyToString($this); + } + + /** + * Closes each attached stream. + * + * {@inheritdoc} + */ + public function close() + { + $this->pos = $this->current = 0; + + foreach ($this->streams as $stream) { + $stream->close(); + } + + $this->streams = []; + } + + /** + * Detaches each attached stream + * + * {@inheritdoc} + */ + public function detach() + { + $this->close(); + $this->detached = true; + } + + public function attach($stream) + { + throw new CannotAttachException(); + } + + public function tell() + { + return $this->pos; + } + + /** + * Tries to calculate the size by adding the size of each stream. + * + * If any of the streams do not return a valid number, then the size of the + * append stream cannot be determined and null is returned. + * + * {@inheritdoc} + */ + public function getSize() + { + $size = 0; + + foreach ($this->streams as $stream) { + $s = $stream->getSize(); + if ($s === null) { + return null; + } + $size += $s; + } + + return $size; + } + + public function eof() + { + return !$this->streams || + ($this->current >= count($this->streams) - 1 && + $this->streams[$this->current]->eof()); + } + + /** + * Attempts to seek to the given position. Only supports SEEK_SET. + * + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (!$this->seekable || $whence !== SEEK_SET) { + return false; + } + + $success = true; + $this->pos = $this->current = 0; + + // Rewind each stream + foreach ($this->streams as $stream) { + if (!$stream->seek(0)) { + $success = false; + } + } + + if (!$success) { + return false; + } + + // Seek to the actual position by reading from each stream + while ($this->pos < $offset && !$this->eof()) { + $this->read(min(8096, $offset - $this->pos)); + } + + return $this->pos == $offset; + } + + /** + * Reads from all of the appended streams until the length is met or EOF. + * + * {@inheritdoc} + */ + public function read($length) + { + $buffer = ''; + $total = count($this->streams) - 1; + $remaining = $length; + + while ($remaining > 0) { + // Progress to the next stream if needed. + if ($this->streams[$this->current]->eof()) { + if ($this->current == $total) { + break; + } + $this->current++; + } + $buffer .= $this->streams[$this->current]->read($remaining); + $remaining = $length - strlen($buffer); + } + + $this->pos += strlen($buffer); + + return $buffer; + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return false; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function write($string) + { + return false; + } + + public function getMetadata($key = null) + { + return $key ? null : []; + } +} diff --git a/Framework/lib/Streams/AsyncReadStream.php b/Framework/lib/Streams/AsyncReadStream.php new file mode 100755 index 0000000..25ad960 --- /dev/null +++ b/Framework/lib/Streams/AsyncReadStream.php @@ -0,0 +1,207 @@ +isReadable() || !$buffer->isWritable()) { + throw new \InvalidArgumentException( + 'Buffer must be readable and writable' + ); + } + + if (isset($config['size'])) { + $this->size = $config['size']; + } + + static $callables = ['pump', 'drain']; + foreach ($callables as $check) { + if (isset($config[$check])) { + if (!is_callable($config[$check])) { + throw new \InvalidArgumentException( + $check . ' must be callable' + ); + } + $this->{$check} = $config[$check]; + } + } + + $this->hwm = $buffer->getMetadata('hwm'); + + // Cannot drain when there's no high water mark. + if ($this->hwm === null) { + $this->drain = null; + } + + $this->stream = $buffer; + } + + /** + * Factory method used to create new async stream and an underlying buffer + * if no buffer is provided. + * + * This function accepts the same options as AsyncReadStream::__construct, + * but added the following key value pairs: + * + * - buffer: (StreamInterface) Buffer used to buffer data. If none is + * provided, a default buffer is created. + * - hwm: (int) High water mark to use if a buffer is created on your + * behalf. + * - max_buffer: (int) If provided, wraps the utilized buffer in a + * DroppingStream decorator to ensure that buffer does not exceed a given + * length. When exceeded, the stream will begin dropping data. Set the + * max_buffer to 0, to use a NullStream which does not store data. + * - write: (callable) A function that is invoked when data is written + * to the underlying buffer. The function accepts the buffer as the first + * argument, and the data being written as the second. The function MUST + * return the number of bytes that were written or false to let writers + * know to slow down. + * - drain: (callable) See constructor documentation. + * - pump: (callable) See constructor documentation. + * + * @param array $options Associative array of options. + * + * @return array Returns an array containing the buffer used to buffer + * data, followed by the ready to use AsyncReadStream object. + */ + public static function create(array $options = []) + { + $maxBuffer = isset($options['max_buffer']) + ? $options['max_buffer'] + : null; + + if ($maxBuffer === 0) { + $buffer = new NullStream(); + } elseif (isset($options['buffer'])) { + $buffer = $options['buffer']; + } else { + $hwm = isset($options['hwm']) ? $options['hwm'] : 16384; + $buffer = new BufferStream($hwm); + } + + if ($maxBuffer > 0) { + $buffer = new DroppingStream($buffer, $options['max_buffer']); + } + + // Call the on_write callback if an on_write function was provided. + if (isset($options['write'])) { + $onWrite = $options['write']; + $buffer = FnStream::decorate($buffer, [ + 'write' => function ($string) use ($buffer, $onWrite) { + $result = $buffer->write($string); + $onWrite($buffer, $string); + return $result; + } + ]); + } + + return [$buffer, new self($buffer, $options)]; + } + + public function getSize() + { + return $this->size; + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + return false; + } + + public function read($length) + { + if (!$this->needsDrain && $this->drain) { + $this->needsDrain = $this->stream->getSize() >= $this->hwm; + } + + $result = $this->stream->read($length); + + // If we need to drain, then drain when the buffer is empty. + if ($this->needsDrain && $this->stream->getSize() === 0) { + $this->needsDrain = false; + $drainFn = $this->drain; + $drainFn($this->stream); + } + + $resultLen = strlen($result); + + // If a pump was provided, the buffer is still open, and not enough + // data was given, then block until the data is provided. + if ($this->pump && $resultLen < $length) { + $pumpFn = $this->pump; + $result .= $pumpFn($length - $resultLen); + } + + return $result; + } +} diff --git a/Framework/lib/Streams/BufferStream.php b/Framework/lib/Streams/BufferStream.php new file mode 100755 index 0000000..0fffbd6 --- /dev/null +++ b/Framework/lib/Streams/BufferStream.php @@ -0,0 +1,138 @@ +hwm = $hwm; + } + + public function __toString() + { + return $this->getContents(); + } + + public function getContents() + { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + public function close() + { + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + } + + public function attach($stream) + { + throw new CannotAttachException(); + } + + public function getSize() + { + return strlen($this->buffer); + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return true; + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + return false; + } + + public function eof() + { + return strlen($this->buffer) === 0; + } + + public function tell() + { + return false; + } + + /** + * Reads data from the buffer. + */ + public function read($length) + { + $currentLength = strlen($this->buffer); + + if ($length >= $currentLength) { + // No need to slice the buffer because we don't have enough data. + $result = $this->buffer; + $this->buffer = ''; + } else { + // Slice up the result to provide a subset of the buffer. + $result = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + } + + return $result; + } + + /** + * Writes data to the buffer. + */ + public function write($string) + { + $this->buffer .= $string; + + if (strlen($this->buffer) >= $this->hwm) { + return false; + } + + return strlen($string); + } + + public function getMetadata($key = null) + { + if ($key == 'hwm') { + return $this->hwm; + } + + return $key ? null : []; + } +} diff --git a/Framework/lib/Streams/CachingStream.php b/Framework/lib/Streams/CachingStream.php new file mode 100755 index 0000000..60bb905 --- /dev/null +++ b/Framework/lib/Streams/CachingStream.php @@ -0,0 +1,122 @@ +remoteStream = $stream; + $this->stream = $target ?: new Stream(fopen('php://temp', 'r+')); + } + + public function getSize() + { + return max($this->stream->getSize(), $this->remoteStream->getSize()); + } + + /** + * {@inheritdoc} + * @throws SeekException When seeking with SEEK_END or when seeking + * past the total size of the buffer stream + */ + public function seek($offset, $whence = SEEK_SET) + { + if ($whence == SEEK_SET) { + $byte = $offset; + } elseif ($whence == SEEK_CUR) { + $byte = $offset + $this->tell(); + } else { + return false; + } + + // You cannot skip ahead past where you've read from the remote stream + if ($byte > $this->stream->getSize()) { + throw new SeekException( + $this, + $byte, + sprintf('Cannot seek to byte %d when the buffered stream only' + . ' contains %d bytes', $byte, $this->stream->getSize()) + ); + } + + return $this->stream->seek($byte); + } + + public function read($length) + { + // Perform a regular read on any previously read data from the buffer + $data = $this->stream->read($length); + $remaining = $length - strlen($data); + + // More data was requested so read from the remote stream + if ($remaining) { + // If data was written to the buffer in a position that would have + // been filled from the remote stream, then we must skip bytes on + // the remote stream to emulate overwriting bytes from that + // position. This mimics the behavior of other PHP stream wrappers. + $remoteData = $this->remoteStream->read( + $remaining + $this->skipReadBytes + ); + + if ($this->skipReadBytes) { + $len = strlen($remoteData); + $remoteData = substr($remoteData, $this->skipReadBytes); + $this->skipReadBytes = max(0, $this->skipReadBytes - $len); + } + + $data .= $remoteData; + $this->stream->write($remoteData); + } + + return $data; + } + + public function write($string) + { + // When appending to the end of the currently read stream, you'll want + // to skip bytes from being read from the remote stream to emulate + // other stream wrappers. Basically replacing bytes of data of a fixed + // length. + $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); + if ($overflow > 0) { + $this->skipReadBytes += $overflow; + } + + return $this->stream->write($string); + } + + public function eof() + { + return $this->stream->eof() && $this->remoteStream->eof(); + } + + /** + * Close both the remote stream and buffer stream + */ + public function close() + { + $this->remoteStream->close() && $this->stream->close(); + } +} diff --git a/Framework/lib/Streams/DroppingStream.php b/Framework/lib/Streams/DroppingStream.php new file mode 100755 index 0000000..56ee80c --- /dev/null +++ b/Framework/lib/Streams/DroppingStream.php @@ -0,0 +1,42 @@ +stream = $stream; + $this->maxLength = $maxLength; + } + + public function write($string) + { + $diff = $this->maxLength - $this->stream->getSize(); + + // Begin returning false when the underlying stream is too large. + if ($diff <= 0) { + return false; + } + + // Write the stream or a subset of the stream if needed. + if (strlen($string) < $diff) { + return $this->stream->write($string); + } + + $this->stream->write(substr($string, 0, $diff)); + + return false; + } +} diff --git a/Framework/lib/Streams/Exception/CannotAttachException.php b/Framework/lib/Streams/Exception/CannotAttachException.php new file mode 100755 index 0000000..e631b9f --- /dev/null +++ b/Framework/lib/Streams/Exception/CannotAttachException.php @@ -0,0 +1,4 @@ +stream = $stream; + $msg = $msg ?: 'Could not seek the stream to position ' . $pos; + parent::__construct($msg); + } + + /** + * @return StreamInterface + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/Framework/lib/Streams/FnStream.php b/Framework/lib/Streams/FnStream.php new file mode 100755 index 0000000..6b5872d --- /dev/null +++ b/Framework/lib/Streams/FnStream.php @@ -0,0 +1,147 @@ +methods = $methods; + + // Create the functions on the class + foreach ($methods as $name => $fn) { + $this->{'_fn_' . $name} = $fn; + } + } + + /** + * Lazily determine which methods are not implemented. + * @throws \BadMethodCallException + */ + public function __get($name) + { + throw new \BadMethodCallException(str_replace('_fn_', '', $name) + . '() is not implemented in the FnStream'); + } + + /** + * The close method is called on the underlying stream only if possible. + */ + public function __destruct() + { + if (isset($this->_fn_close)) { + call_user_func($this->_fn_close); + } + } + + /** + * Adds custom functionality to an underlying stream by intercepting + * specific method calls. + * + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure + * + * @return FnStream + */ + public static function decorate(StreamInterface $stream, array $methods) + { + // If any of the required methods were not provided, then simply + // proxy to the decorated stream. + foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { + $methods[$diff] = [$stream, $diff]; + } + + return new self($methods); + } + + public function __toString() + { + return call_user_func($this->_fn___toString); + } + + public function close() + { + return call_user_func($this->_fn_close); + } + + public function detach() + { + return call_user_func($this->_fn_detach); + } + + public function attach($stream) + { + return call_user_func($this->_fn_attach, $stream); + } + + public function getSize() + { + return call_user_func($this->_fn_getSize); + } + + public function tell() + { + return call_user_func($this->_fn_tell); + } + + public function eof() + { + return call_user_func($this->_fn_eof); + } + + public function isSeekable() + { + return call_user_func($this->_fn_isSeekable); + } + + public function seek($offset, $whence = SEEK_SET) + { + return call_user_func($this->_fn_seek, $offset, $whence); + } + + public function isWritable() + { + return call_user_func($this->_fn_isWritable); + } + + public function write($string) + { + return call_user_func($this->_fn_write, $string); + } + + public function isReadable() + { + return call_user_func($this->_fn_isReadable); + } + + public function read($length) + { + return call_user_func($this->_fn_read, $length); + } + + public function getContents() + { + return call_user_func($this->_fn_getContents); + } + + public function getMetadata($key = null) + { + return call_user_func($this->_fn_getMetadata, $key); + } +} diff --git a/Framework/lib/Streams/GuzzleStreamWrapper.php b/Framework/lib/Streams/GuzzleStreamWrapper.php new file mode 100755 index 0000000..47ca01c --- /dev/null +++ b/Framework/lib/Streams/GuzzleStreamWrapper.php @@ -0,0 +1,117 @@ +isReadable()) { + $mode = $stream->isWritable() ? 'r+' : 'r'; + } elseif ($stream->isWritable()) { + $mode = 'w'; + } else { + throw new \InvalidArgumentException('The stream must be readable, ' + . 'writable, or both.'); + } + + return fopen('guzzle://stream', $mode, null, stream_context_create([ + 'guzzle' => ['stream' => $stream], + ])); + } + + /** + * Registers the stream wrapper if needed + */ + public static function register() + { + if (!in_array('guzzle', stream_get_wrappers())) { + stream_wrapper_register('guzzle', __CLASS__); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) + { + $options = stream_context_get_options($this->context); + + if (!isset($options['guzzle']['stream'])) { + return false; + } + + $this->mode = $mode; + $this->stream = $options['guzzle']['stream']; + + return true; + } + + public function stream_read($count) + { + return $this->stream->read($count); + } + + public function stream_write($data) + { + return (int) $this->stream->write($data); + } + + public function stream_tell() + { + return $this->stream->tell(); + } + + public function stream_eof() + { + return $this->stream->eof(); + } + + public function stream_seek($offset, $whence) + { + return $this->stream->seek($offset, $whence); + } + + public function stream_stat() + { + static $modeMap = [ + 'r' => 33060, + 'r+' => 33206, + 'w' => 33188, + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->stream->getSize() ?: 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } +} diff --git a/Framework/lib/Streams/InflateStream.php b/Framework/lib/Streams/InflateStream.php new file mode 100755 index 0000000..978af21 --- /dev/null +++ b/Framework/lib/Streams/InflateStream.php @@ -0,0 +1,27 @@ +stream = new Stream($resource); + } +} diff --git a/Framework/lib/Streams/LazyOpenStream.php b/Framework/lib/Streams/LazyOpenStream.php new file mode 100755 index 0000000..6242ee7 --- /dev/null +++ b/Framework/lib/Streams/LazyOpenStream.php @@ -0,0 +1,37 @@ +filename = $filename; + $this->mode = $mode; + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + return Stream::factory(Utils::open($this->filename, $this->mode)); + } +} diff --git a/Framework/lib/Streams/LimitStream.php b/Framework/lib/Streams/LimitStream.php new file mode 100755 index 0000000..e9fad98 --- /dev/null +++ b/Framework/lib/Streams/LimitStream.php @@ -0,0 +1,161 @@ +stream = $stream; + $this->setLimit($limit); + $this->setOffset($offset); + } + + public function eof() + { + // Always return true if the underlying stream is EOF + if ($this->stream->eof()) { + return true; + } + + // No limit and the underlying stream is not at EOF + if ($this->limit == -1) { + return false; + } + + $tell = $this->stream->tell(); + if ($tell === false) { + return false; + } + + return $tell >= $this->offset + $this->limit; + } + + /** + * Returns the size of the limited subset of data + * {@inheritdoc} + */ + public function getSize() + { + if (null === ($length = $this->stream->getSize())) { + return null; + } elseif ($this->limit == -1) { + return $length - $this->offset; + } else { + return min($this->limit, $length - $this->offset); + } + } + + /** + * Allow for a bounded seek on the read limited stream + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if ($whence !== SEEK_SET || $offset < 0) { + return false; + } + + $offset += $this->offset; + + if ($this->limit !== -1) { + if ($offset > $this->offset + $this->limit) { + $offset = $this->offset + $this->limit; + } + } + + return $this->stream->seek($offset); + } + + /** + * Give a relative tell() + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell() - $this->offset; + } + + /** + * Set the offset to start limiting from + * + * @param int $offset Offset to seek to and begin byte limiting from + * + * @return self + * @throws SeekException + */ + public function setOffset($offset) + { + $current = $this->stream->tell(); + + if ($current !== $offset) { + // If the stream cannot seek to the offset position, then read to it + if (!$this->stream->seek($offset)) { + if ($current > $offset) { + throw new SeekException($this, $offset); + } else { + $this->stream->read($offset - $current); + } + } + } + + $this->offset = $offset; + + return $this; + } + + /** + * Set the limit of bytes that the decorator allows to be read from the + * stream. + * + * @param int $limit Number of bytes to allow to be read from the stream. + * Use -1 for no limit. + * @return self + */ + public function setLimit($limit) + { + $this->limit = $limit; + + return $this; + } + + public function read($length) + { + if ($this->limit == -1) { + return $this->stream->read($length); + } + + // Check if the current position is less than the total allowed + // bytes + original offset + $remaining = ($this->offset + $this->limit) - $this->stream->tell(); + if ($remaining > 0) { + // Only return the amount of requested data, ensuring that the byte + // limit is not exceeded + return $this->stream->read(min($remaining, $length)); + } else { + return false; + } + } +} diff --git a/Framework/lib/Streams/MetadataStreamInterface.php b/Framework/lib/Streams/MetadataStreamInterface.php new file mode 100755 index 0000000..c1433ad --- /dev/null +++ b/Framework/lib/Streams/MetadataStreamInterface.php @@ -0,0 +1,11 @@ +stream->attach($stream); + } +} diff --git a/Framework/lib/Streams/NullStream.php b/Framework/lib/Streams/NullStream.php new file mode 100755 index 0000000..aeda6be --- /dev/null +++ b/Framework/lib/Streams/NullStream.php @@ -0,0 +1,79 @@ +source = $source; + $this->size = isset($options['size']) ? $options['size'] : null; + $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->buffer = new BufferStream(); + } + + public function __toString() + { + return Utils::copyToString($this); + } + + public function close() + { + $this->detach(); + } + + public function detach() + { + $this->tellPos = false; + $this->source = null; + } + + public function attach($stream) + { + throw new CannotAttachException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + return $this->tellPos; + } + + public function eof() + { + return !$this->source; + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + return false; + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + return false; + } + + public function isReadable() + { + return true; + } + + public function read($length) + { + $data = $this->buffer->read($length); + $readLen = strlen($data); + $this->tellPos += $readLen; + $remaining = $length - $readLen; + + if ($remaining) { + $this->pump($remaining); + $data .= $this->buffer->read($remaining); + $this->tellPos += strlen($data) - $readLen; + } + + return $data; + } + + public function getContents() + { + $result = ''; + while (!$this->eof()) { + $result .= $this->read(1000000); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!$key) { + return $this->metadata; + } + + return isset($this->metadata[$key]) ? $this->metadata[$key] : null; + } + + private function pump($length) + { + if ($this->source) { + do { + $data = call_user_func($this->source, $length); + if ($data === false || $data === null) { + $this->source = null; + return; + } + $this->buffer->write($data); + $length -= strlen($data); + } while ($length > 0); + } + } +} diff --git a/Framework/lib/Streams/Stream.php b/Framework/lib/Streams/Stream.php new file mode 100755 index 0000000..81c8041 --- /dev/null +++ b/Framework/lib/Streams/Stream.php @@ -0,0 +1,261 @@ + [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, + ], + ]; + + /** + * Create a new stream based on the input type. + * + * This factory accepts the same associative array of options as described + * in the constructor. + * + * @param resource|string|StreamInterface $resource Entity body data + * @param array $options Additional options + * + * @return Stream + * @throws \InvalidArgumentException if the $resource arg is not valid. + */ + public static function factory($resource = '', array $options = []) + { + $type = gettype($resource); + + if ($type == 'string') { + $stream = fopen('php://temp', 'r+'); + if ($resource !== '') { + fwrite($stream, $resource); + fseek($stream, 0); + } + return new self($stream, $options); + } + + if ($type == 'resource') { + return new self($resource, $options); + } + + if ($resource instanceof StreamInterface) { + return $resource; + } + + if ($type == 'object' && method_exists($resource, '__toString')) { + return self::factory((string) $resource, $options); + } + + if (is_callable($resource)) { + return new PumpStream($resource, $options); + } + + if ($resource instanceof \Iterator) { + return new PumpStream(function () use ($resource) { + if (!$resource->valid()) { + return false; + } + $result = $resource->current(); + $resource->next(); + return $result; + }, $options); + } + + throw new \InvalidArgumentException('Invalid resource type: ' . $type); + } + + /** + * This constructor accepts an associative array of options. + * + * - size: (int) If a read stream would otherwise have an indeterminate + * size, but the size is known due to foreknownledge, then you can + * provide that size, in bytes. + * - metadata: (array) Any additional metadata to return when the metadata + * of the stream is accessed. + * + * @param resource $stream Stream resource to wrap. + * @param array $options Associative array of options. + * + * @throws \InvalidArgumentException if the stream is not a stream resource + */ + public function __construct($stream, $options = []) + { + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Stream must be a resource'); + } + + if (isset($options['size'])) { + $this->size = $options['size']; + } + + $this->customMetadata = isset($options['metadata']) + ? $options['metadata'] + : []; + + $this->attach($stream); + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + public function __toString() + { + if (!$this->stream) { + return ''; + } + + $this->seek(0); + + return (string) stream_get_contents($this->stream); + } + + public function getContents() + { + return $this->stream ? stream_get_contents($this->stream) : ''; + } + + public function close() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + + $this->detach(); + } + + public function detach() + { + $result = $this->stream; + $this->stream = $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function attach($stream) + { + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); + $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); + $this->uri = $this->getMetadata('uri'); + } + + public function getSize() + { + if ($this->size !== null) { + return $this->size; + } + + if (!$this->stream) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + return $this->size; + } + + return null; + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function eof() + { + return !$this->stream || feof($this->stream); + } + + public function tell() + { + return $this->stream ? ftell($this->stream) : false; + } + + public function setSize($size) + { + $this->size = $size; + + return $this; + } + + public function seek($offset, $whence = SEEK_SET) + { + return $this->seekable + ? fseek($this->stream, $offset, $whence) === 0 + : false; + } + + public function read($length) + { + return $this->readable ? fread($this->stream, $length) : false; + } + + public function write($string) + { + // We can't know the size after writing anything + $this->size = null; + + return $this->writable ? fwrite($this->stream, $string) : false; + } + + public function getMetadata($key = null) + { + if (!$this->stream) { + return $key ? null : []; + } elseif (!$key) { + return $this->customMetadata + stream_get_meta_data($this->stream); + } elseif (isset($this->customMetadata[$key])) { + return $this->customMetadata[$key]; + } + + $meta = stream_get_meta_data($this->stream); + + return isset($meta[$key]) ? $meta[$key] : null; + } +} diff --git a/Framework/lib/Streams/StreamDecoratorTrait.php b/Framework/lib/Streams/StreamDecoratorTrait.php new file mode 100755 index 0000000..0f551d3 --- /dev/null +++ b/Framework/lib/Streams/StreamDecoratorTrait.php @@ -0,0 +1,144 @@ +stream = $stream; + } + + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of a decorator (e.g., LazyOpenStream). + */ + public function __get($name) + { + if ($name == 'stream') { + $this->stream = $this->createStream(); + return $this->stream; + } + + throw new \UnexpectedValueException("$name not found on class"); + } + + public function __toString() + { + try { + $this->seek(0); + return $this->getContents(); + } catch (\Exception $e) { + // Really, PHP? https://bugs.php.net/bug.php?id=53648 + trigger_error('StreamDecorator::__toString exception: ' + . (string) $e, E_USER_ERROR); + return ''; + } + } + + public function getContents() + { + return Utils::copyToString($this); + } + + /** + * Allow decorators to implement custom methods + * + * @param string $method Missing method name + * @param array $args Method arguments + * + * @return mixed + */ + public function __call($method, array $args) + { + $result = call_user_func_array(array($this->stream, $method), $args); + + // Always return the wrapped object if the result is a return $this + return $result === $this->stream ? $this : $result; + } + + public function close() + { + $this->stream->close(); + } + + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + public function detach() + { + return $this->stream->detach(); + } + + public function attach($stream) + { + throw new CannotAttachException(); + } + + public function getSize() + { + return $this->stream->getSize(); + } + + public function eof() + { + return $this->stream->eof(); + } + + public function tell() + { + return $this->stream->tell(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function isWritable() + { + return $this->stream->isWritable(); + } + + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + public function seek($offset, $whence = SEEK_SET) + { + return $this->stream->seek($offset, $whence); + } + + public function read($length) + { + return $this->stream->read($length); + } + + public function write($string) + { + return $this->stream->write($string); + } + + /** + * Implement in subclasses to dynamically create streams when requested. + * + * @return StreamInterface + * @throws \BadMethodCallException + */ + protected function createStream() + { + throw new \BadMethodCallException('createStream() not implemented in ' + . get_class($this)); + } +} diff --git a/Framework/lib/Streams/StreamInterface.php b/Framework/lib/Streams/StreamInterface.php new file mode 100755 index 0000000..fd19c6f --- /dev/null +++ b/Framework/lib/Streams/StreamInterface.php @@ -0,0 +1,159 @@ +eof()) { + $buf = $stream->read(1048576); + if ($buf === false) { + break; + } + $buffer .= $buf; + } + return $buffer; + } + + $len = 0; + while (!$stream->eof() && $len < $maxLen) { + $buf = $stream->read($maxLen - $len); + if ($buf === false) { + break; + } + $buffer .= $buf; + $len = strlen($buffer); + } + + return $buffer; + } + + /** + * Copy the contents of a stream into another stream until the given number + * of bytes have been read. + * + * @param StreamInterface $source Stream to read from + * @param StreamInterface $dest Stream to write to + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + */ + public static function copyToStream( + StreamInterface $source, + StreamInterface $dest, + $maxLen = -1 + ) { + if ($maxLen === -1) { + while (!$source->eof()) { + if (!$dest->write($source->read(1048576))) { + break; + } + } + return; + } + + $bytes = 0; + while (!$source->eof()) { + $buf = $source->read($maxLen - $bytes); + if (!($len = strlen($buf))) { + break; + } + $bytes += $len; + $dest->write($buf); + if ($bytes == $maxLen) { + break; + } + } + } + + /** + * Calculate a hash of a Stream + * + * @param StreamInterface $stream Stream to calculate the hash for + * @param string $algo Hash algorithm (e.g. md5, crc32, etc) + * @param bool $rawOutput Whether or not to use raw output + * + * @return string Returns the hash of the stream + * @throws SeekException + */ + public static function hash( + StreamInterface $stream, + $algo, + $rawOutput = false + ) { + $pos = $stream->tell(); + + if ($pos > 0 && !$stream->seek(0)) { + throw new SeekException($stream); + } + + $ctx = hash_init($algo); + while (!$stream->eof()) { + hash_update($ctx, $stream->read(1048576)); + } + + $out = hash_final($ctx, (bool) $rawOutput); + $stream->seek($pos); + + return $out; + } + + /** + * Read a line from the stream up to the maximum allowed buffer length + * + * @param StreamInterface $stream Stream to read from + * @param int $maxLength Maximum buffer length + * @param string $eol Line ending + * + * @return string|bool + */ + public static function readline(StreamInterface $stream, $maxLength = null, $eol = PHP_EOL) + { + $buffer = ''; + $size = 0; + $negEolLen = -strlen($eol); + + while (!$stream->eof()) { + if (false === ($byte = $stream->read(1))) { + return $buffer; + } + $buffer .= $byte; + // Break when a new line is found or the max length - 1 is reached + if (++$size == $maxLength || substr($buffer, $negEolLen) === $eol) { + break; + } + } + + return $buffer; + } + + /** + * Alias of GuzzleHttp\Stream\Stream::factory. + * + * @param mixed $resource Resource to create + * @param array $options Associative array of stream options defined in + * {@see \GuzzleHttp\Stream\Stream::__construct} + * + * @return StreamInterface + * + * @see GuzzleHttp\Stream\Stream::factory + * @see GuzzleHttp\Stream\Stream::__construct + */ + public static function create($resource, array $options = []) + { + return Stream::factory($resource, $options); + } +} diff --git a/Framework/phpunit.xml b/Framework/phpunit.xml new file mode 100644 index 0000000..26ff8b2 --- /dev/null +++ b/Framework/phpunit.xml @@ -0,0 +1,35 @@ + + + + + + tests + + + + + + + + + + src + + src/autoload.php + + + + diff --git a/Framework/src/Backends/AwsRestBackend.php b/Framework/src/Backends/AwsRestBackend.php new file mode 100644 index 0000000..7c52e0d --- /dev/null +++ b/Framework/src/Backends/AwsRestBackend.php @@ -0,0 +1,47 @@ +requestBuilder = $requestBuilder; + $this->curl = $curl; + } + + public function deleteObject(string $objectName) + { + $objectUri = new \Timetabio\S3Helper\ValueObjects\Uri('/' . $objectName); + $request = $this->requestBuilder->buildRequest('DELETE', $objectUri); + + $response = $this->curl->delete( + new Uri($request->getUrl()), + null, + $request->getHeaders() + ); + + $code = $response->getCode(); + + if ($code !== 204) { + throw new \Exception('invalid response code from amazon s3 (' . $code . ')'); + } + } + } +} diff --git a/Framework/src/Backends/AwsS3Backend.php b/Framework/src/Backends/AwsS3Backend.php new file mode 100644 index 0000000..da30215 --- /dev/null +++ b/Framework/src/Backends/AwsS3Backend.php @@ -0,0 +1,45 @@ +uploadBuilder = $uploadBuilder; + $this->uriBuilder = $uriBuilder; + $this->maxFileSize = $maxFileSize; + } + + public function getEndpoint(): string + { + return $this->uriBuilder->buildBucketUrl(); + } + + public function createUploadParams(FileUpload $file): array + { + return $this->uploadBuilder->buildUploadParams($file, $this->maxFileSize, 5 * 60); + } + } +} diff --git a/Framework/src/Backends/DomBackend.php b/Framework/src/Backends/DomBackend.php new file mode 100644 index 0000000..c3b19fc --- /dev/null +++ b/Framework/src/Backends/DomBackend.php @@ -0,0 +1,39 @@ +fileBackend = $fileBackend; + } + + public function loadXml(string $fileName): Document + { + $document = new Document; + + $document->loadXML($this->fileBackend->read($fileName)); + + return $document; + } + + public function loadHtml(string $fileName): Document + { + $document = new Document; + + $document->loadHTML($this->fileBackend->read($fileName), LIBXML_HTML_NOIMPLIED); + + return $document; + } + } +} diff --git a/Framework/src/Backends/ElasticBackend.php b/Framework/src/Backends/ElasticBackend.php new file mode 100644 index 0000000..1a59c40 --- /dev/null +++ b/Framework/src/Backends/ElasticBackend.php @@ -0,0 +1,79 @@ +client = $client; + $this->index = $index; + } + + public function indexDocument(string $type, string $id, array $document): array + { + return $this->client->index([ + 'index' => $this->index, + 'type' => $type, + 'id' => $id, + 'body' => $document + ]); + } + + public function indexRoutedDocument(string $type, string $id, string $routing, array $document): array + { + return $this->client->index([ + 'index' => $this->index, + 'type' => $type, + 'id' => $id, + 'routing' => $routing, + 'body' => $document + ]); + } + + public function indexChildDocument(string $type, string $id, string $parent, array $document): array + { + return $this->client->index([ + 'index' => $this->index, + 'type' => $type, + 'id' => $id, + 'parent' => $parent, + 'body' => $document + ]); + } + + public function deleteDocument(string $type, string $id): array + { + return $this->client->delete([ + 'index' => $this->index, + 'type' => $type, + 'id' => $id + ]); + } + + public function search(string $type, int $from, int $size, array $body): array + { + return $this->client->search([ + 'index' => $this->index, + 'type' => $type, + 'from' => $from, + 'size' => $size, + 'body' => $body + ]); + } + } +} diff --git a/Framework/src/Backends/FileBackend.php b/Framework/src/Backends/FileBackend.php new file mode 100644 index 0000000..fb9ff7a --- /dev/null +++ b/Framework/src/Backends/FileBackend.php @@ -0,0 +1,41 @@ +exists($file)) { + throw new \Exception('file ' . $file . ' not found'); + } + + return file_get_contents($file); + } + + public function write(string $fileName, string $content) + { + try { + file_put_contents($fileName, $content); + } catch (\Throwable $error) { + throw new \Exception('could not write to file ' . $fileName, 0, $error); + } + } + + public function append(string $fileName, string $content) + { + try { + file_put_contents($fileName, $content, FILE_APPEND); + } catch (\Throwable $error) { + throw new \Exception('could not write to file ' . $fileName, 0, $error); + } + } + + public function exists(string $file): bool + { + return file_exists($file); + } + } +} diff --git a/Framework/src/Backends/InkBackend.php b/Framework/src/Backends/InkBackend.php new file mode 100644 index 0000000..411b1ae --- /dev/null +++ b/Framework/src/Backends/InkBackend.php @@ -0,0 +1,58 @@ +parser = $parser; + $this->generator = $generator; + $this->textGenerator = $textGenerator; + $this->previewTransformation = $previewTransformation; + } + + public function process(string $text): InkResult + { + $ast = $this->parser->parse(str_replace("\r\n", PHP_EOL, $text)); + + return new InkResult( + $this->generator->generate($ast), + $this->generator->generate($this->previewTransformation->apply($ast)), + $this->textGenerator->generate($ast) + ); + } + } +} diff --git a/Framework/src/Backends/MailBackendInterface.php b/Framework/src/Backends/MailBackendInterface.php new file mode 100644 index 0000000..f0d31d4 --- /dev/null +++ b/Framework/src/Backends/MailBackendInterface.php @@ -0,0 +1,13 @@ +curl = $curl; + $this->endpoint = $endpoint; + $this->apiKey = $apiKey; + $this->sender = $sender; + } + + public function send(MailInterface $mail) + { + $credentials = new BasicAuth('api', $this->apiKey); + $url = new Uri($this->endpoint); + + $params = [ + 'to' => (string) $mail->getRecipient(), + 'subject' => $mail->getSubject(), + 'from' => $this->sender, + 'html' => $mail->render() + ]; + + $response = $this->curl->post($url, $params, $credentials); + + if ($response->getCode() !== 200) { + $exception = new \RuntimeException('error sending email via mailgun', $response->getCode()); + + $this->getLogger()->emergency($exception); + + throw $exception; + } + } + } +} diff --git a/Framework/src/Backends/PostgresBackend.php b/Framework/src/Backends/PostgresBackend.php new file mode 100644 index 0000000..7b87118 --- /dev/null +++ b/Framework/src/Backends/PostgresBackend.php @@ -0,0 +1,109 @@ +pdo = $pdo; + } + + public function fetch(string $sql, array $parameters = []) + { + $stmt = $this->executeStatement($sql, $parameters); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if ($result === false) { + return null; + } + + return $result; + } + + public function fetchAll(string $sql, array $parameters = []) + { + $stmt = $this->executeStatement($sql, $parameters); + + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + public function fetchColumn(string $sql, array $parameters = []) + { + $stmt = $this->executeStatement($sql, $parameters); + + return $stmt->fetch(\PDO::FETCH_COLUMN); + } + + public function fetchColumns(string $sql, array $parameters = []): \Traversable + { + $stmt = $this->executeStatement($sql, $parameters); + + $stmt->setFetchMode(\PDO::FETCH_COLUMN, 0); + + return $stmt; + } + + public function insert(string $sql, array $parameters = []) + { + return $this->fetch($sql, $parameters); + } + + public function execute(string $sql, array $parameters = []) + { + $this->executeStatement($sql, $parameters); + } + + public function lastInsertId(string $name) + { + return $this->pdo->lastInsertId($name); + } + + public function beginTransaction() + { + $this->pdo->beginTransaction(); + } + + public function commitTransaction() + { + $this->pdo->commit(); + } + + public function rollbackTransaction() + { + $this->pdo->rollBack(); + } + + private function executeStatement(string $sql, array $parameters): \PDOStatement + { + $statement = $this->pdo->prepare($sql); + + foreach ($parameters as $key => $value) { + if ($value instanceof \Timetabio\Framework\Pdo\Value\ValueInterface) { + $statement->bindValue($key, $value->getValue(), $value->getType()); + } else { + $statement->bindValue($key, $value); + } + } + + try { + $statement->execute(); + } catch (\Exception $exception) { + // Ignore invalid_text_representation (See: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html) + // TODO: this might be dangerous for inserts with UUIDs or Enums + if ($exception->getCode() !== '22P02') { + throw $exception; + } + } + + return $statement; + } + } +} diff --git a/Framework/src/Backends/Streams/AbstractStreamWrapper.php b/Framework/src/Backends/Streams/AbstractStreamWrapper.php new file mode 100644 index 0000000..648db64 --- /dev/null +++ b/Framework/src/Backends/Streams/AbstractStreamWrapper.php @@ -0,0 +1,74 @@ +transformPath($path); + + return stat($path); + } + + public function stream_stat(): array + { + return fstat($this->resource); + } + + public function stream_open(string $path, string $mode = null): bool + { + $path = $this->transformPath($path); + + $this->resource = fopen($path, $mode); + + return (bool) $this->resource; + } + + public function stream_read(int $count) + { + return fread($this->resource, $count); + } + + public function stream_eof(): bool + { + return feof($this->resource); + } + + protected function transformPath(string $path): string + { + $uri = parse_url($path); + $filePath = static::getBasePath(); + + if (isset($uri['host'])) { + $filePath .= '/' . $uri['host']; + } + + if (isset($uri['path'])) { + $filePath .= $uri['path']; + } + + return $filePath; + } + + abstract protected static function getBasePath(): string; + + abstract protected static function setBasePath(string $basePath); + + abstract protected static function getProtocol(): string; + } +} diff --git a/Framework/src/Bootstrap/AbstractBootstrapper.php b/Framework/src/Bootstrap/AbstractBootstrapper.php new file mode 100644 index 0000000..0286100 --- /dev/null +++ b/Framework/src/Bootstrap/AbstractBootstrapper.php @@ -0,0 +1,138 @@ +configuration = $this->buildConfiguration(); + $this->factory = $this->buildFactory(); + + $this->buildErrorHandler()->register(); + + $this->request = $this->buildRequest(); + $this->router = $this->buildRouter(); + + $this->doBootstrap(); + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + public function getRouter(): RouterInterface + { + return $this->router; + } + + protected function getConfiguration(): ConfigurationInterface + { + return $this->configuration; + } + + protected function getFactory(): MasterFactoryInterface + { + return $this->factory; + } + + private function buildRequest(): RequestInterface + { + $uri = $this->buildUri(); + + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + case 'HEAD': + return new GetRequest($uri, $_SERVER, $_COOKIE); + case 'POST': + return new PostRequest($uri, $_SERVER, $_COOKIE, $this->parseBody(), $_FILES); + case 'PATCH': + return new PatchRequest($uri, $_SERVER, $_COOKIE, $this->parseBody()); + case 'PUT': + return new PutRequest($uri, $_SERVER, $_COOKIE, $this->parseBody()); + case 'DELETE': + return new DeleteRequest($uri, $_SERVER, $_COOKIE); + } + + throw new \RuntimeException('unsupported request method "' . $_SERVER['REQUEST_METHOD'] . '"'); + } + + private function parseBody(): array + { + if (!empty($_POST)) { + return $_POST; + } + + $input = file_get_contents('php://input', null, null, null, 1024 ** 2); + + switch ($_SERVER['CONTENT_TYPE']) { + case 'application/json': + return json_decode($input, true); + case 'application/x-www-form-urlencoded': + $data = []; + parse_str($input, $data); + return $data; + } + + return []; + } + + private function buildUri(): Uri + { + $scheme = 'http://'; + + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $scheme = 'https://'; + } + + return new Uri($scheme . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); + } + + abstract protected function doBootstrap(); + + abstract protected function buildConfiguration(): ConfigurationInterface; + + abstract protected function buildFactory(): MasterFactoryInterface; + + abstract protected function buildRouter(): RouterInterface; + + abstract protected function buildErrorHandler(): AbstractErrorHandler; + } +} +// @codeCoverageIgnoreEnd diff --git a/Framework/src/Configuration/Configuration.php b/Framework/src/Configuration/Configuration.php new file mode 100644 index 0000000..9143dfa --- /dev/null +++ b/Framework/src/Configuration/Configuration.php @@ -0,0 +1,84 @@ +fileName = $fileName; + } + + public function has(string $key): bool + { + $this->load(); + + return isset($this->data[$key]); + } + + public function get(string $key) + { + $this->load(); + + if (!isset($this->data[$key])) { + throw new \Exception('configuration key "' . $key . '" not found'); + } + + return $this->data[$key]; + } + + public function isDevelopmentMode(): bool + { + return $this->get('isDevelopmentMode'); + } + + public function getRedisHost(): string + { + return $this->get('redisHost'); + } + + public function getRedisPort(): int + { + return $this->get('redisPort'); + } + + public function getSlackEndpoint(): Uri + { + return new Uri($this->get('slackEndpoint')); + } + + private function load() + { + if ($this->isLoaded) { + return; + } + + try { + $this->data = parse_ini_file($this->fileName, false, INI_SCANNER_TYPED); + } catch (\Throwable $e) { + throw new \Exception('error parsing configuration file "' . $this->fileName . '"', 0, $e); + } + + $this->isLoaded = true; + } + } +} diff --git a/Framework/src/Configuration/ConfigurationInterface.php b/Framework/src/Configuration/ConfigurationInterface.php new file mode 100644 index 0000000..5d63b82 --- /dev/null +++ b/Framework/src/Configuration/ConfigurationInterface.php @@ -0,0 +1,20 @@ +model = $model; + $this->preHandler = $preHandler; + $this->requestHandler = $requestHandler; + $this->queryHandler = $queryHandler; + $this->commandHandler = $commandHandler; + $this->transformationHandler = $transformationHandler; + $this->responseHandlerInterface = $responseHandlerInterface; + $this->postHandlerInterface = $postHandlerInterface; + $this->response = $response; + } + + public function processRequest(RequestInterface $request): ResponseInterface + { + $this->preHandler->execute($request, $this->model); + $this->requestHandler->execute($request, $this->model); + + $this->queryHandler->execute($this->model); + $this->commandHandler->execute($this->model); + + $this->response->setBody($this->transformationHandler->execute($this->model)); + + $this->responseHandlerInterface->execute($this->response, $this->model); + $this->postHandlerInterface->execute($this->model); + + return $this->response; + } + } +} + diff --git a/Framework/src/Controllers/ControllerInterface.php b/Framework/src/Controllers/ControllerInterface.php new file mode 100644 index 0000000..d4067da --- /dev/null +++ b/Framework/src/Controllers/ControllerInterface.php @@ -0,0 +1,14 @@ +getType() . ' ' . $this->getValue(); + } + } +} diff --git a/Framework/src/Curl/Credentials/BasicAuth.php b/Framework/src/Curl/Credentials/BasicAuth.php new file mode 100644 index 0000000..3773491 --- /dev/null +++ b/Framework/src/Curl/Credentials/BasicAuth.php @@ -0,0 +1,35 @@ +username = $username; + $this->password = $password; + } + + public function getType(): string + { + return 'Basic'; + } + + public function getValue(): string + { + return base64_encode($this->username . ':' . $this->password); + } + } +} diff --git a/Framework/src/Curl/Credentials/BearerToken.php b/Framework/src/Curl/Credentials/BearerToken.php new file mode 100644 index 0000000..a01ac63 --- /dev/null +++ b/Framework/src/Curl/Credentials/BearerToken.php @@ -0,0 +1,29 @@ +token = $token; + } + + public function getType(): string + { + return 'Bearer'; + } + + public function getValue(): string + { + return $this->token; + } + } +} diff --git a/Framework/src/Curl/Credentials/CredentialsInterface.php b/Framework/src/Curl/Credentials/CredentialsInterface.php new file mode 100644 index 0000000..952e3df --- /dev/null +++ b/Framework/src/Curl/Credentials/CredentialsInterface.php @@ -0,0 +1,13 @@ +handler = $handler; + } + + public function post(Uri $url, array $params = [], CredentialsInterface $credentials = null): Response + { + return $this->handler->executeRequest($url, new Post, $params, $credentials); + } + + public function patch(Uri $url, array $params = [], CredentialsInterface $credentials = null): Response + { + return $this->handler->executeRequest($url, new Patch, $params, $credentials); + } + + public function get(Uri $url, CredentialsInterface $credentials = null): Response + { + return $this->handler->executeRequest($url, new Get, [], $credentials); + } + + public function delete(Uri $url, CredentialsInterface $credentials = null, array $headers = []): Response + { + return $this->handler->executeRequest($url, new Delete, [], $credentials, $headers); + } + + public function head(Uri $url, CredentialsInterface $credentials = null): Response + { + return $this->handler->executeRequest($url, new Head, [], $credentials); + } + } +} diff --git a/Framework/src/Curl/CurlHandler.php b/Framework/src/Curl/CurlHandler.php new file mode 100644 index 0000000..b714423 --- /dev/null +++ b/Framework/src/Curl/CurlHandler.php @@ -0,0 +1,108 @@ +handle = curl_init(); + $this->headers = new RequestHeaders; + + foreach ($headers as $key => $value) { + $this->headers->set($key, $value); + } + + $this->enableReturnTransfer(); + $this->setUrl($url); + $this->setRequestMethod($requestMethod); + + if ($credentials instanceof CredentialsInterface) { + $this->setCredentials($credentials); + } + + if ($requestMethod->hasBody()) { + $this->setPostFields($params); + } + + $this->setHeaders(); + + $body = curl_exec($this->handle); + $code = curl_getinfo($this->handle, CURLINFO_HTTP_CODE); + $error = curl_error($this->handle); + + if (!empty($error)) { + throw new \RuntimeException($error, curl_errno($this->handle)); + } + + curl_close($this->handle); + + return new Response($code, $body); + } + + private function setUrl(string $url) + { + $this->setOption(CURLOPT_URL, $url); + } + + private function enableReturnTransfer() + { + $this->setOption(CURLOPT_RETURNTRANSFER, true); + } + + private function setCredentials(CredentialsInterface $credentials) + { + $this->headers->set('authorization', (string) $credentials); + } + + private function setHeaders() + { + $this->setOption(CURLOPT_HTTPHEADER, $this->headers->toArray()); + } + + private function setPostFields(array $params) + { + $this->setOption(CURLOPT_POST, true); + $this->setOption(CURLOPT_POSTFIELDS, http_build_query($params)); + } + + private function setRequestMethod(RequestMethodInterface $requestMethod) + { + $this->setOption(CURLOPT_CUSTOMREQUEST, (string) $requestMethod); + + if ($requestMethod instanceof Head) { + $this->setOption(CURLOPT_NOBODY, true); + } + } + + private function setOption(int $opt, $value) + { + curl_setopt($this->handle, $opt, $value); + } + } +} +// @codeCoverageIgnoreEnd diff --git a/Framework/src/Curl/RequestHeaders.php b/Framework/src/Curl/RequestHeaders.php new file mode 100644 index 0000000..b10dbdc --- /dev/null +++ b/Framework/src/Curl/RequestHeaders.php @@ -0,0 +1,24 @@ +headers[strtolower($name)] = $name . ':' . $value; + } + + public function toArray(): array + { + return array_values($this->headers); + } + } +} diff --git a/Framework/src/Curl/RequestMethods/Delete.php b/Framework/src/Curl/RequestMethods/Delete.php new file mode 100644 index 0000000..fc08ef7 --- /dev/null +++ b/Framework/src/Curl/RequestMethods/Delete.php @@ -0,0 +1,19 @@ +code = $code; + $this->body = $body; + } + + public function getCode(): int + { + return $this->code; + } + + public function getBody(): string + { + return $this->body; + } + + public function getJsonDecodedBody() + { + return json_decode($this->body, true); + } + } +} diff --git a/Framework/src/DataStore/DataStoreInterface.php b/Framework/src/DataStore/DataStoreInterface.php new file mode 100644 index 0000000..588752c --- /dev/null +++ b/Framework/src/DataStore/DataStoreInterface.php @@ -0,0 +1,44 @@ +redis = $redis; + $this->redisHost = $redisHost; + $this->redisPort = $redisPort; + } + + public function has(string $key): bool + { + $this->connect(); + + return $this->redis->exists($key); + } + + public function get(string $key): ?string + { + $this->connect(); + + return $this->redis->get($key); + } + + public function set(string $key, $value) + { + $this->connect(); + + $this->redis->set($key, $value); + } + + public function remove(string $key) + { + $this->connect(); + + $this->redis->delete($key); + } + + public function setTimeout(string $key, int $ttl) + { + $this->connect(); + + $this->redis->expire($key, $ttl); + } + + public function removeTimeout(string $key) + { + $this->connect(); + + $this->redis->persist($key); + } + + public function pop(string $key) + { + $this->connect(); + + return $this->redis->rPop($key); + } + + public function push(string $key, $value) + { + $this->connect(); + + $this->redis->lPush($key, $value); + } + + public function addToSet(string $key, string $value) + { + $this->connect(); + + $this->redis->sAdd($key, $value); + } + + public function hasInSet(string $key, string $value): bool + { + $this->connect(); + + return $this->redis->sIsMember($key, $value); + } + + public function removeFromSet(string $key, string $value) + { + $this->connect(); + + return $this->redis->sRem($key, $value); + } + + public function setInHash(string $hash, string $key, string $value) + { + $this->connect(); + + $this->redis->hSet($hash, $key, $value); + } + + public function getFromHash(string $hash, string $key) + { + $this->connect(); + + return $this->redis->hGet($hash, $key); + } + + public function hasInHash(string $hash, string $key): bool + { + $this->connect(); + + return $this->redis->hExists($hash, $key); + } + + public function removeFromHash(string $hash, string $key) + { + $this->connect(); + + $this->redis->hDel($hash, $key); + } + + public function zLpop(string $key) + { + $this->connect(); + + $result = $this->redis->multi() + ->zRange($key, 0, 0) + ->zRemRangeByRank($key, 0, 0) + ->exec(); + + if (!isset($result[0][0])) { + return null; + } + + return $result[0][0]; + } + + public function zAdd(string $key, int $score, string $value) + { + $this->connect(); + + $this->redis->zAdd($key, $score, $value); + } + + public function sCard(string $key): int + { + $this->connect(); + + return $this->redis->sCard($key); + } + + private function connect() + { + if ($this->redis->isConnected()) { + return; + } + + $this->redis->connect($this->redisHost, $this->redisPort); + } + } +} diff --git a/Framework/src/Dom/Document.php b/Framework/src/Dom/Document.php new file mode 100644 index 0000000..3dd30ae --- /dev/null +++ b/Framework/src/Dom/Document.php @@ -0,0 +1,100 @@ +registerNodeClasses(); + } + + public function loadHTML($source, $options = 0) + { + parent::loadHTML(mb_convert_encoding($source, 'HTML-ENTITIES', 'UTF-8'), $options); + + $errors = libxml_get_errors(); + libxml_clear_errors(); + + /** @var \LibXMLError $error */ + foreach ($errors as $error) { + // ignore XML_HTML_UNKNOWN_TAG + if ($error->code === 801) { + continue; + } + + throw new Exception($error->message, $error->code); + } + + return true; + } + + public function createHTMLFragment(string $source): Fragment + { + $document = new Document; + $document->loadHTML('' . $source . '', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + + $fragment = $this->createDocumentFragment(); + + foreach ($document->documentElement->childNodes as $node) { + $fragment->appendChild($this->importNode($node, true)); + } + + return $fragment; + } + + private function registerNodeClasses() + { + $this->registerNodeClass(\DOMNode::class, Node::class); + $this->registerNodeClass(\DOMElement::class, Element::class); + $this->registerNodeClass(\DOMDocumentFragment::class, Fragment::class); + } + + public function query(string $query, \DOMNode $targetNode = null): \DOMNodeList + { + return $this->getXpath()->query($query, $targetNode); + } + + /** + * @return Node|null + */ + public function queryOne(string $query, \DOMNode $targetNode = null) + { + return $this->query($query, $targetNode)->item(0); + } + + public function getXpath(): \DOMXPath + { + if ($this->xpath === null) { + $this->xpath = new \DOMXPath($this); + } + + return $this->xpath; + } + + public function getMainElement(): \DOMElement + { + return $this->queryOne('//main'); + } + + public function importDocument(\DOMDocument $document): \DOMNode + { + return $this->importNode($document->documentElement, true); + } + } +} diff --git a/Framework/src/Dom/Element.php b/Framework/src/Dom/Element.php new file mode 100644 index 0000000..371d99c --- /dev/null +++ b/Framework/src/Dom/Element.php @@ -0,0 +1,52 @@ +appendChild($this->ownerDocument->createTextNode($text)); + } + + public function appendChild(\DOMNode $node) + { + if ($node instanceof \DOMDocumentFragment && !$node->childNodes->length) { + return $node; + } + + return parent::appendChild($node); + } + + public function queryOne(string $query) + { + return $this->ownerDocument->queryOne($query, $this); + } + + public function query(string $query): \DOMNodeList + { + return $this->ownerDocument->query($query, $this); + } + + public function setClassName(string $className) + { + $this->setAttribute('class', $className); + } + + public function getAttribute($name) + { + if (!$this->hasAttribute($name)) { + return null; + } + + return parent::getAttribute($name); + } + } +} diff --git a/Framework/src/Dom/Exception.php b/Framework/src/Dom/Exception.php new file mode 100644 index 0000000..bab8dab --- /dev/null +++ b/Framework/src/Dom/Exception.php @@ -0,0 +1,11 @@ +appendChild($this->ownerDocument->createTextNode($text)); + } + } +} diff --git a/Framework/src/ErrorHandlers/AbstractErrorHandler.php b/Framework/src/ErrorHandlers/AbstractErrorHandler.php new file mode 100644 index 0000000..787aa3a --- /dev/null +++ b/Framework/src/ErrorHandlers/AbstractErrorHandler.php @@ -0,0 +1,42 @@ +registered = true; + } + + public function __destruct() + { + if (!$this->registered) { + return; + } + + restore_error_handler(); + restore_exception_handler(); + } + + // @codeCoverageIgnoreEnd + + public function handleError(int $errno, string $errstr, string $errfile = '', int $errline = 0) + { + throw new \ErrorException($errstr, -1, $errno, $errfile, $errline); + } + + abstract public function handleException(\Throwable $exception); + } +} diff --git a/Framework/src/Exceptions/FileUploadException.php b/Framework/src/Exceptions/FileUploadException.php new file mode 100644 index 0000000..1638785 --- /dev/null +++ b/Framework/src/Exceptions/FileUploadException.php @@ -0,0 +1,44 @@ +getMessageForCode($code), $code); + } + + /** + * @param int $code + * @return string + * @link http://php.net/manual/en/features.file-upload.errors.php + */ + private function getMessageForCode(int $code): string + { + switch ($code) { + case UPLOAD_ERR_INI_SIZE: + return 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'; + case UPLOAD_ERR_FORM_SIZE: + return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'; + case UPLOAD_ERR_PARTIAL: + return 'The uploaded file was only partially uploaded.'; + case UPLOAD_ERR_NO_FILE: + return 'No file was uploaded.'; + case UPLOAD_ERR_NO_TMP_DIR: + return 'Missing a temporary folder.'; + case UPLOAD_ERR_CANT_WRITE: + return 'Failed to write file to disk.'; + case UPLOAD_ERR_EXTENSION: + return 'A PHP extension stopped the file upload.'; + } + + return 'An unknown error occurred'; + } + } +} diff --git a/Framework/src/Exceptions/RouterException.php b/Framework/src/Exceptions/RouterException.php new file mode 100644 index 0000000..d506753 --- /dev/null +++ b/Framework/src/Exceptions/RouterException.php @@ -0,0 +1,11 @@ +masterFactory = $masterFactory; + } + + protected function getMasterFactory(): MasterFactoryInterface + { + return $this->masterFactory; + } + } +} diff --git a/Framework/src/Factories/BackendFactory.php b/Framework/src/Factories/BackendFactory.php new file mode 100644 index 0000000..f32d78e --- /dev/null +++ b/Framework/src/Factories/BackendFactory.php @@ -0,0 +1,98 @@ +getMasterFactory()->createCurl(), + $this->getMasterFactory()->getConfiguration()->get('mailgunEndpoint'), + $this->getMasterFactory()->getConfiguration()->get('mailgunApiKey'), + $this->getMasterFactory()->getConfiguration()->get('mailSender') + ); + } + + public function createPostgresBackend(): \Timetabio\Framework\Backends\PostgresBackend + { + if ($this->postgresBackend === null) { + $config = $this->getMasterFactory()->getConfiguration(); + $pdo = new \PDO($config->get('postgresDsn')); + + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + $this->postgresBackend = new \Timetabio\Framework\Backends\PostgresBackend($pdo); + } + + return $this->postgresBackend; + } + + public function createDomBackend(): \Timetabio\Framework\Backends\DomBackend + { + return new \Timetabio\Framework\Backends\DomBackend( + $this->getMasterFactory()->createFileBackend() + ); + } + + public function createAwsS3Backend(): \Timetabio\Framework\Backends\AwsS3Backend + { + $config = $this->getMasterFactory()->getConfiguration(); + + return new \Timetabio\Framework\Backends\AwsS3Backend( + $this->getMasterFactory()->createS3HelperUploadBuilder(), + $this->getMasterFactory()->createS3HelperUriBuilder(), + $config->get('s3MaxUploadSize') + ); + } + + public function createAwsRestBackend(): \Timetabio\Framework\Backends\AwsRestBackend + { + return new \Timetabio\Framework\Backends\AwsRestBackend( + $this->getMasterFactory()->createS3HelperRequestBuilder(), + $this->getMasterFactory()->createCurl() + ); + } + + public function createInkBackend(): \Timetabio\Framework\Backends\InkBackend + { + $factory = new \Ink\Factory; + $options = new \Ink\Generators\Dom\GeneratorOptions; + + return new \Timetabio\Framework\Backends\InkBackend( + $factory->createParser(), + $factory->createDomGenerator($options), + $factory->createPreviewTransformation(), + $factory->createTextGenerator() + ); + } + + public function createElasticBackend(): \Timetabio\Framework\Backends\ElasticBackend + { + $config = $this->getMasterFactory()->getConfiguration(); + + $hosts = [ + $config->get('elasticHost') + ]; + + return new \Timetabio\Framework\Backends\ElasticBackend( + \Elasticsearch\ClientBuilder::create()->setHosts($hosts)->build(), + $config->get('elasticIndex') + ); + } + } +} diff --git a/Framework/src/Factories/ChildFactoryInterface.php b/Framework/src/Factories/ChildFactoryInterface.php new file mode 100644 index 0000000..06efa72 --- /dev/null +++ b/Framework/src/Factories/ChildFactoryInterface.php @@ -0,0 +1,11 @@ +getMasterFactory()->createCurlHandler() + ); + } + + public function createCurlHandler(): \Timetabio\Framework\Curl\CurlHandler + { + return new \Timetabio\Framework\Curl\CurlHandler; + } + + public function createRedisBackend(): \Timetabio\Framework\DataStore\RedisBackend + { + if ($this->redisBackend === null) { + $this->redisBackend = new \Timetabio\Framework\DataStore\RedisBackend( + new \Redis, + $this->getMasterFactory()->getConfiguration()->getRedisHost(), + $this->getMasterFactory()->getConfiguration()->getRedisPort() + ); + } + + return $this->redisBackend; + } + + public function createGettext(): \Timetabio\Framework\Translation\Gettext + { + return new \Timetabio\Framework\Translation\Gettext; + } + + public function createS3HelperUploadBuilder(): \Timetabio\S3Helper\Builders\UploadBuilder + { + return new \Timetabio\S3Helper\Builders\UploadBuilder( + $this->createS3HelperConfiguration() + ); + } + + public function createS3HelperRequestBuilder(): \Timetabio\S3Helper\Builders\RequestBuilder + { + return new \Timetabio\S3Helper\Builders\RequestBuilder( + $this->createS3HelperConfiguration(), + $this->getMasterFactory()->createS3HelperUriBuilder() + ); + } + + public function createS3HelperUriBuilder(): \Timetabio\S3Helper\Builders\UriBuilder + { + return new \Timetabio\S3Helper\Builders\UriBuilder( + $this->getMasterFactory()->getConfiguration()->get('s3Bucket') + ); + } + + private function createS3HelperConfiguration(): \Timetabio\S3Helper\ValueObjects\Configuration + { + $config = $this->getMasterFactory()->getConfiguration(); + + return new \Timetabio\S3Helper\ValueObjects\Configuration( + $config->get('s3AccessKey'), + $config->get('s3AccessSecret'), + $config->get('s3Region'), + $config->get('s3Bucket') + ); + } + } +} diff --git a/Framework/src/Factories/LoggerFactory.php b/Framework/src/Factories/LoggerFactory.php new file mode 100644 index 0000000..d58305e --- /dev/null +++ b/Framework/src/Factories/LoggerFactory.php @@ -0,0 +1,46 @@ +getMasterFactory()->createCurl(), + $this->getMasterFactory()->getConfiguration()->getSlackEndpoint() + ); + } + + public function createNsaLogger(): \Timetabio\Framework\Logging\Loggers\NsaLogger + { + return new \Timetabio\Framework\Logging\Loggers\NsaLogger( + $this->getMasterFactory()->createFileBackend(), + $this->getMasterFactory()->getConfiguration()->get('nsaLog') + ); + } + + public function createLogger(): \Timetabio\Framework\Logging\Loggers\Logger + { + return new \Timetabio\Framework\Logging\Loggers\Logger; + } + + public function createLoggers(): \Timetabio\Framework\Logging\Loggers\Logger + { + if ($this->logger === null) { + $this->logger = $this->createLogger(); + // $this->logger->addLogger($this->createSlackLogger()); + $this->logger->addLogger($this->createNsaLogger()); + } + + return $this->logger; + } + } +} diff --git a/Framework/src/Factories/MasterFactory.php b/Framework/src/Factories/MasterFactory.php new file mode 100644 index 0000000..c661ffd --- /dev/null +++ b/Framework/src/Factories/MasterFactory.php @@ -0,0 +1,74 @@ +configuration = $configuration; + } + + public function registerFactory(ChildFactoryInterface $factory) + { + $reflection = new \ReflectionClass($factory); + + $factory->setMasterFactory($this); + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $methodName = $method->getName(); + + if (substr($methodName, 0, 6) !== 'create') { + continue; + } + + if (isset($this->methods[$methodName])) { + throw new \RuntimeException('method "' . $methodName . '" is already registered'); + } + + $this->methods[$methodName] = $factory; + } + } + + public function getConfiguration(): ConfigurationInterface + { + return $this->configuration; + } + + public function __call(string $name, array $arguments) + { + if (!isset($this->methods[$name])) { + throw new \InvalidArgumentException('no method found for name "' . $name . '"'); + } + + $factory = $this->methods[$name]; + $result = call_user_func_array([$factory, $name], $arguments); + + if ($result instanceof LoggerAwareInterface) { + $result->setLogger($this->createLoggers()); + } + + if ($result instanceof TranslatorAwareInterface) { + $result->setTranslator($this->createGettext()); + } + + return $result; + } + } +} diff --git a/Framework/src/Factories/MasterFactoryInterface.php b/Framework/src/Factories/MasterFactoryInterface.php new file mode 100644 index 0000000..4ab53cb --- /dev/null +++ b/Framework/src/Factories/MasterFactoryInterface.php @@ -0,0 +1,20 @@ +bootstrapper = $bootstrapper; + } + + public function run() + { + $request = $this->bootstrapper->getRequest(); + + $this->bootstrapper->getRouter() + ->route($request) + ->processRequest($request) + ->send(); + } + } +} +// @codeCoverageIgnoreEnd diff --git a/Framework/src/Handlers/CommandHandlerInterface.php b/Framework/src/Handlers/CommandHandlerInterface.php new file mode 100644 index 0000000..f9224fc --- /dev/null +++ b/Framework/src/Handlers/CommandHandlerInterface.php @@ -0,0 +1,13 @@ +type = $explodedValue[0]; + $this->token = $explodedValue[1]; + } + + public function getType(): string + { + return $this->type; + } + + public function isBearer(): bool + { + return $this->type === 'Bearer'; + } + + public function getToken(): string + { + return $this->token; + } + + public function getBearerToken() + { + if (!$this->isBearer()) { + return null; + } + + return $this->getToken(); + } + } +} diff --git a/Framework/src/Http/Redirect/AbstractRedirect.php b/Framework/src/Http/Redirect/AbstractRedirect.php new file mode 100644 index 0000000..ab5b66d --- /dev/null +++ b/Framework/src/Http/Redirect/AbstractRedirect.php @@ -0,0 +1,26 @@ +uri = $uri; + } + + public function getUri(): Uri + { + return $this->uri; + } + } +} diff --git a/Framework/src/Http/Redirect/PermanentRedirect.php b/Framework/src/Http/Redirect/PermanentRedirect.php new file mode 100644 index 0000000..bdebafe --- /dev/null +++ b/Framework/src/Http/Redirect/PermanentRedirect.php @@ -0,0 +1,17 @@ +uri = $uri; + $this->server = $server; + $this->cookies = $cookies; + } + + public function getUri(): Uri + { + return $this->uri; + } + + public function getQueryParam(string $param): string + { + return $this->uri->getQueryParam($param); + } + + public function hasQueryParam(string $param): bool + { + return $this->uri->hasQueryParam($param); + } + + public function getUserAgent(): string + { + return $this->server['HTTP_USER_AGENT']; + } + + public function getUserIp(): string + { + return $this->server['REMOTE_ADDR']; + } + + public function getAuthorization(): Authorization + { + return new Authorization($this->server['HTTP_AUTHORIZATION']); + } + + public function hasAuthorization(): bool + { + try { + $this->getAuthorization(); + return true; + } catch (\Throwable $e) { + return false; + } + } + + public function hasCookie(string $name): bool + { + return isset($this->cookies[$name]); + } + + public function getCookie(string $name): string + { + if (!isset($this->cookies[$name])) { + throw new \Exception('cookie with name "' . $name . '" not found'); + } + + return $this->cookies[$name]; + } + + public function getLanguage(): LanguageInterface + { + // $header = new \http\Header('Accept-Language', $this->server['HTTP_ACCEPT_LANGUAGE'] ?? ''); + // $language = $header->negotiate(['en', 'de']); + + // switch ($language) { + // case 'de': + // return new \Timetabio\Framework\Languages\German; + // } + + return new \Timetabio\Framework\Languages\English; + } + + public function isDnt(): bool + { + if (!isset($this->server['HTTP_DNT'])) { + return false; + } + + return $this->server['HTTP_DNT'] === '1'; + } + } +} diff --git a/Framework/src/Http/Request/AbstractWriteRequest.php b/Framework/src/Http/Request/AbstractWriteRequest.php new file mode 100644 index 0000000..30662e0 --- /dev/null +++ b/Framework/src/Http/Request/AbstractWriteRequest.php @@ -0,0 +1,37 @@ +body = $body; + } + + public function hasParam(string $name): bool + { + return isset($this->body[$name]); + } + + public function getParam(string $name) + { + if (!isset($this->body[$name])) { + throw new \Exception('param with name "' . $name . '" was not found in request'); + } + + return $this->body[$name]; + } + } +} diff --git a/Framework/src/Http/Request/DeleteRequest.php b/Framework/src/Http/Request/DeleteRequest.php new file mode 100644 index 0000000..de36ac4 --- /dev/null +++ b/Framework/src/Http/Request/DeleteRequest.php @@ -0,0 +1,11 @@ +files = $files; + } + + public function hasFile(string $name): bool + { + return isset($this->files[$name]); + } + + // @codeCoverageIgnoreStart + public function getFile(string $name): UploadedFile + { + if (!isset($this->files[$name])) { + throw new \Exception('file with name "' . $name . '" was not found in request'); + } + + return new UploadedFile($this->files[$name]); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/Framework/src/Http/Request/PutRequest.php b/Framework/src/Http/Request/PutRequest.php new file mode 100644 index 0000000..56dde5d --- /dev/null +++ b/Framework/src/Http/Request/PutRequest.php @@ -0,0 +1,11 @@ +statusCode = $statusCode; + } + + public function setHeader(Header $header) + { + $this->headers[$header->getName()] = $header; + } + + public function setRedirect(RedirectInterface $redirect) + { + $this->redirect = $redirect; + } + + public function setCookie(Cookie $cookie) + { + $this->cookies[$cookie->getName()] = $cookie; + } + + public function setBody(string $body) + { + $this->body = $body; + } + + public function send() + { + $this->setHeader(new Header('content-type', $this->getContentType())); + + foreach ($this->headers as $header) { + header((string) $header); + } + + foreach ($this->cookies as $cookie) { + setcookie( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpires(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + $cookie->isHttpOnly() + ); + } + + if ($this->statusCode instanceof StatusCodeInterface) { + http_response_code($this->statusCode->getCode()); + } + + if ($this->redirect instanceof RedirectInterface) { + header( + 'location: ' . $this->redirect->getUri(), + true, + $this->redirect->getStatusCode()->getCode() + ); + } + + echo $this->body; + } + + abstract protected function getContentType(): string; + } +} diff --git a/Framework/src/Http/Response/HtmlResponse.php b/Framework/src/Http/Response/HtmlResponse.php new file mode 100644 index 0000000..876ae49 --- /dev/null +++ b/Framework/src/Http/Response/HtmlResponse.php @@ -0,0 +1,14 @@ +logger = $logger; + } + + protected function getLogger(): Logger + { + return $this->logger; + } + } +} diff --git a/Framework/src/Logging/Loggers/Logger.php b/Framework/src/Logging/Loggers/Logger.php new file mode 100644 index 0000000..aa1c30d --- /dev/null +++ b/Framework/src/Logging/Loggers/Logger.php @@ -0,0 +1,49 @@ +loggers[] = $logger; + } + + public function error(\Throwable $error) + { + $this->log(new ErrorLog($error)); + } + + public function emergency(\Throwable $error) + { + $this->log(new EmergencyLog($error)); + } + + public function log(AbstractLog $log) + { + foreach ($this->loggers as $logger) { + if (!$logger->handles($log)) { + continue; + } + + $logger->log($log); + } + } + + public function handles(AbstractLog $log): bool + { + return true; + } + } +} diff --git a/Framework/src/Logging/Loggers/LoggerInterface.php b/Framework/src/Logging/Loggers/LoggerInterface.php new file mode 100644 index 0000000..11ada0d --- /dev/null +++ b/Framework/src/Logging/Loggers/LoggerInterface.php @@ -0,0 +1,15 @@ +fileBackend = $fileBackend; + $this->fileName = $fileName; + } + + public function log(AbstractLog $log) + { + $this->fileBackend->append( + $this->fileName, + '[' . date('d.m.Y H:i:s') . '] ' . $log . PHP_EOL . PHP_EOL + ); + } + + public function handles(AbstractLog $log): bool + { + return true; + } + } +} diff --git a/Framework/src/Logging/Loggers/SlackLogger.php b/Framework/src/Logging/Loggers/SlackLogger.php new file mode 100644 index 0000000..64e8ddf --- /dev/null +++ b/Framework/src/Logging/Loggers/SlackLogger.php @@ -0,0 +1,50 @@ +curl = $curl; + $this->endpoint = $endpoint; + } + + public function log(AbstractLog $log) + { + $text = [ + '*' . $log->getMessage() . '*', + 'in `' . $log->getFile() . ':' . $log->getLine() . '`', + '```' . $log->getStringTrace() . '```' + ]; + + $this->curl->post($this->endpoint, [ + 'payload' => json_encode([ + 'text' => implode(PHP_EOL, $text) + ]), + ]); + } + + public function handles(AbstractLog $log): bool + { + return $log instanceof EmergencyLog; + } + } +} diff --git a/Framework/src/Logging/Logs/AbstractLog.php b/Framework/src/Logging/Logs/AbstractLog.php new file mode 100644 index 0000000..3c295bb --- /dev/null +++ b/Framework/src/Logging/Logs/AbstractLog.php @@ -0,0 +1,46 @@ +exception = $exception; + } + + public function getMessage(): string + { + return $this->exception->getMessage(); + } + + public function getStringTrace(): string + { + return $this->exception->getTraceAsString(); + } + + public function getFile(): string + { + return $this->exception->getFile(); + } + + public function getLine(): int + { + return $this->exception->getLine(); + } + + public function __toString(): string + { + return get_class($this->exception) . ': "' . $this->getMessage() . '"' . + ' in file ' . $this->getFile() . ' on line ' . $this->getLine() . + PHP_EOL . $this->getStringTrace(); + } + } +} diff --git a/Framework/src/Logging/Logs/EmergencyLog.php b/Framework/src/Logging/Logs/EmergencyLog.php new file mode 100644 index 0000000..d8198b3 --- /dev/null +++ b/Framework/src/Logging/Logs/EmergencyLog.php @@ -0,0 +1,11 @@ +data = $data; + } + + public function has(string $key): bool + { + return isset($this->data[$key]); + } + + public function get(string $key) + { + if (!isset($this->data[$key])) { + throw new \Exception('key "' . $key . '" not found in map'); + } + + return $this->data[$key]; + } + + public function set(string $key, $value) + { + $this->data[$key] = $value; + } + + public function remove(string $key) + { + unset($this->data[$key]); + } + + public function getIterator() + { + return new \ArrayIterator($this->data); + } + } +} diff --git a/Framework/src/Map/MapInterface.php b/Framework/src/Map/MapInterface.php new file mode 100644 index 0000000..66dd218 --- /dev/null +++ b/Framework/src/Map/MapInterface.php @@ -0,0 +1,10 @@ +value = $value; + } + + public function getType(): int + { + return \PDO::PARAM_BOOL; + } + + public function getValue(): bool + { + return $this->value; + } + } +} diff --git a/Framework/src/Pdo/Value/NullValue.php b/Framework/src/Pdo/Value/NullValue.php new file mode 100644 index 0000000..40e368b --- /dev/null +++ b/Framework/src/Pdo/Value/NullValue.php @@ -0,0 +1,19 @@ +routers[] = $router; + } + + public function route(RequestInterface $request): ControllerInterface + { + foreach ($this->routers as $router) { + if (!$router->canHandle($request)) { + continue; + } + + try { + return $router->route($request); + } catch (RouterException $e) { + continue; + } + } + + throw new RouterException('no route found for "' . $request->getUri()->getPath() . '"'); + } + + public function canHandle(RequestInterface $request): bool + { + return true; + } + } +} diff --git a/Framework/src/Routers/RouterInterface.php b/Framework/src/Routers/RouterInterface.php new file mode 100644 index 0000000..d86dec9 --- /dev/null +++ b/Framework/src/Routers/RouterInterface.php @@ -0,0 +1,16 @@ +buildLookupMessage($message, $context); + $translation = gettext($lookupMessage); + + if ($translation === $lookupMessage) { + return $message; + } + + return $translation; + } + + private function buildLookupMessage(string $message, string $context = null): string + { + $lookupContext = ''; + + if ($context !== null) { + $lookupContext = $context . "\004"; + } + + return $lookupContext . $message; + } + + public function setUp(string $domain, string $directory) + { + bindtextdomain($domain, $directory); + textdomain($domain); + } + + public function setLanguage(LanguageInterface $language) + { + putenv('LC_ALL=' . $language); + putenv('LANG=' . $language); + putenv('LANGUAGE=' . $language); + setlocale(LC_ALL, $language); + } + } +} diff --git a/Framework/src/Translation/TranslatorAwareInterface.php b/Framework/src/Translation/TranslatorAwareInterface.php new file mode 100644 index 0000000..593bcc5 --- /dev/null +++ b/Framework/src/Translation/TranslatorAwareInterface.php @@ -0,0 +1,11 @@ +translator = $translator; + } + + protected function getTranslator(): TranslatorInterface + { + return $this->translator; + } + } +} diff --git a/Framework/src/Translation/TranslatorInterface.php b/Framework/src/Translation/TranslatorInterface.php new file mode 100644 index 0000000..8fbd85c --- /dev/null +++ b/Framework/src/Translation/TranslatorInterface.php @@ -0,0 +1,15 @@ +name = $name; + $this->value = $value; + $this->path = $path; + $this->expires = $expires; + $this->domain = $domain; + $this->secure = $secure; + $this->httpOnly = $httpOnly; + } + + public function getName(): string + { + return $this->name; + } + + public function getValue(): string + { + return $this->value; + } + + public function getPath(): string + { + return $this->path; + } + + public function getExpires(): int + { + return $this->expires; + } + + public function getDomain(): string + { + return $this->domain; + } + + public function isSecure(): bool + { + return $this->secure; + } + + public function isHttpOnly(): bool + { + return $this->httpOnly; + } + } +} diff --git a/Framework/src/ValueObjects/EmailAddress.php b/Framework/src/ValueObjects/EmailAddress.php new file mode 100644 index 0000000..a3b19b8 --- /dev/null +++ b/Framework/src/ValueObjects/EmailAddress.php @@ -0,0 +1,33 @@ +email = $email; + } + + public function jsonSerialize(): string + { + return (string) $this; + } + + public function __toString(): string + { + return $this->email; + } + } +} diff --git a/Framework/src/ValueObjects/EmailPerson.php b/Framework/src/ValueObjects/EmailPerson.php new file mode 100644 index 0000000..f3bcf69 --- /dev/null +++ b/Framework/src/ValueObjects/EmailPerson.php @@ -0,0 +1,44 @@ +email = $email; + $this->name = $name; + } + + public function getEmail(): EmailAddress + { + return $this->email; + } + + public function getName(): string + { + return $this->name; + } + + public function __toString(): string + { + if ($this->name === '') { + return $this->email; + } + + return $this->name . ' <' . $this->email . '>'; + } + } +} diff --git a/Framework/src/ValueObjects/Header.php b/Framework/src/ValueObjects/Header.php new file mode 100644 index 0000000..2f81c19 --- /dev/null +++ b/Framework/src/ValueObjects/Header.php @@ -0,0 +1,40 @@ +name = strtolower($name); + $this->value = $value; + } + + public function getName(): string + { + return $this->name; + } + + public function getValue(): string + { + return $this->value; + } + + public function __toString() + { + return $this->name . ': ' . $this->value; + } + } +} diff --git a/Framework/src/ValueObjects/InkResult.php b/Framework/src/ValueObjects/InkResult.php new file mode 100644 index 0000000..fbc8ecf --- /dev/null +++ b/Framework/src/ValueObjects/InkResult.php @@ -0,0 +1,46 @@ +body = $body; + $this->preview = $preview; + $this->plainText = $plainText; + } + + public function getBody(): string + { + return $this->body; + } + + public function getPreview(): string + { + return $this->preview; + } + + public function getPlainText(): string + { + return $this->plainText; + } + } +} diff --git a/Framework/src/ValueObjects/StringDateTime.php b/Framework/src/ValueObjects/StringDateTime.php new file mode 100644 index 0000000..aeccb09 --- /dev/null +++ b/Framework/src/ValueObjects/StringDateTime.php @@ -0,0 +1,24 @@ +timestamp = (new \DateTime($time, new \DateTimeZone('UTC')))->getTimestamp(); + } + + public function getTimestamp(): int + { + return $this->timestamp; + } + } +} diff --git a/Framework/src/ValueObjects/Timestamp.php b/Framework/src/ValueObjects/Timestamp.php new file mode 100644 index 0000000..333746d --- /dev/null +++ b/Framework/src/ValueObjects/Timestamp.php @@ -0,0 +1,29 @@ +timestamp = $timestamp; + } + + public function getTimestamp(): int + { + return $this->timestamp; + } + + public function __toString(): string + { + return gmdate('Y-m-d H:i:s.u', $this->timestamp); + } + } +} diff --git a/Framework/src/ValueObjects/Token.php b/Framework/src/ValueObjects/Token.php new file mode 100644 index 0000000..db5acdf --- /dev/null +++ b/Framework/src/ValueObjects/Token.php @@ -0,0 +1,28 @@ +token = $token; + + if ($this->token === null) { + $this->token = bin2hex(openssl_random_pseudo_bytes($length / 2)); + } + } + + public function __toString(): string + { + return $this->token; + } + } +} diff --git a/Framework/src/ValueObjects/UploadedFile.php b/Framework/src/ValueObjects/UploadedFile.php new file mode 100644 index 0000000..c140257 --- /dev/null +++ b/Framework/src/ValueObjects/UploadedFile.php @@ -0,0 +1,71 @@ +file = $file; + } + + /** + * @return string + */ + public function getName() + { + return $this->file['name']; + } + + /** + * @return string + */ + public function getTmpName() + { + return $this->file['tmp_name']; + } + + /** + * @return string + */ + public function getMimeType() + { + return mime_content_type($this->getTmpName()); + } + + /** + * @return string + */ + public function getExtension() + { + return pathinfo($this->getName(), PATHINFO_EXTENSION); + } + } +} +// @codeCoverageIgnoreEnd diff --git a/Framework/src/ValueObjects/Uri.php b/Framework/src/ValueObjects/Uri.php new file mode 100644 index 0000000..c0d11a8 --- /dev/null +++ b/Framework/src/ValueObjects/Uri.php @@ -0,0 +1,104 @@ +path = $parsed['path']; + } + + if (isset($parsed['scheme'])) { + $this->scheme = $parsed['scheme']; + } + + if (isset($parsed['host'])) { + $this->host = $parsed['host']; + } + + if (isset($parsed['query'])) { + parse_str($parsed['query'], $this->query); + } + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPath(): string + { + return $this->path; + } + + public function getExplodedPath(): array + { + return array_slice(explode('/', $this->path), 1); + } + + public function getQueryParam(string $parameter) + { + if (!isset($this->query[$parameter])) { + throw new \Exception('query param "' . $parameter . '" not found'); + } + + return $this->query[$parameter]; + } + + public function hasQueryParam(string $parameter): bool + { + return isset($this->query[$parameter]); + } + + public function __toString() + { + $scheme = $this->scheme; + $query = ''; + + if (!empty($this->query)) { + $query = '?' . http_build_query($this->query); + } + + if ($scheme !== '') { + $scheme .= ':'; + } + + if ($this->host !== '') { + $scheme .= '//'; + } + + return $scheme . $this->host . $this->path . $query; + } + } +} diff --git a/Framework/tests/bootstrap.php b/Framework/tests/bootstrap.php new file mode 100644 index 0000000..aa2d280 --- /dev/null +++ b/Framework/tests/bootstrap.php @@ -0,0 +1,10 @@ +fileBackend = new FileBackend(); + } + + public function testExists() + { + $this->assertTrue($this->fileBackend->exists(__FILE__)); + $this->assertFalse($this->fileBackend->exists(__FILE__ . 'foobarbaz')); + } + + public function testReadWrite() + { + $file = sys_get_temp_dir() . '/' . uniqid(); + $this->fileBackend->write($file, 'foobar'); + $this->assertEquals('foobar', $this->fileBackend->read($file)); + } + + /** + * @expectedException \Exception + */ + public function testReadThrows() + { + $this->fileBackend->read(__DIR__ . '/' . uniqid()); + } + + /** + * @expectedException \Exception + */ + public function testWriteThrows() + { + $this->fileBackend->write(__DIR__, ''); + } + } +} diff --git a/Framework/tests/unit/Configuration/ConfigurationTest.php b/Framework/tests/unit/Configuration/ConfigurationTest.php new file mode 100644 index 0000000..4d24971 --- /dev/null +++ b/Framework/tests/unit/Configuration/ConfigurationTest.php @@ -0,0 +1,79 @@ +getConfig(); + + $this->assertFalse($config->has('qux')); + $this->assertTrue($config->has('foo')); + $this->assertTrue($config->has('bool')); + } + + public function testGetWorks() + { + $config = $this->getConfig(); + + $this->assertEquals('bar', $config->get('foo')); + $this->assertEquals(true, $config->get('bool')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage configuration key "qux" not found + */ + public function testGetThrows() + { + $this->getConfig()->get('qux'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessageRegExp /error parsing configuration file "(.*)"/ + */ + public function testThrowsForInvalidFile() + { + $config = new Configuration(__DIR__ . '/../../data/invalid-config.ini'); + $config->has('baz'); + } + + public function testIsDevelopmentMode() + { + $this->assertTrue($this->getConfig()->isDevelopmentMode()); + } + + public function testGetRedisHost() + { + $this->assertEquals('127.0.0.1', $this->getConfig()->getRedisHost()); + } + + public function testGetRedisPort() + { + $this->assertEquals(6379, $this->getConfig()->getRedisPort()); + } + + public function testGetSlackEndpoint() + { + $endpoint = $this->getConfig()->getSlackEndpoint(); + + $this->assertInstanceOf(Uri::class, $endpoint); + $this->assertEquals('http://slack/endpoint', (string) $endpoint); + } + + private function getConfig() + { + return new Configuration(__DIR__ . '/../../data/config.ini'); + } + } +} diff --git a/Framework/tests/unit/Controllers/AbstractControllerTest.php b/Framework/tests/unit/Controllers/AbstractControllerTest.php new file mode 100644 index 0000000..60d8111 --- /dev/null +++ b/Framework/tests/unit/Controllers/AbstractControllerTest.php @@ -0,0 +1,116 @@ +getMockBuilder(AbstractModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $response + ->expects($this->once()) + ->method('setBody') + ->with('foobar'); + + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $preHandler = $this->getMockBuilder(PreHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $preHandler + ->expects($this->once()) + ->method('execute') + ->with($request, $model); + + $requestHandler = $this->getMockBuilder(RequestHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $requestHandler + ->expects($this->once()) + ->method('execute') + ->with($request, $model); + + $queryHandler = $this->getMockBuilder(QueryHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $queryHandler + ->expects($this->once()) + ->method('execute') + ->with($model); + + $commandHandler = $this->getMockBuilder(CommandHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $commandHandler + ->expects($this->once()) + ->method('execute') + ->with($model); + + $transformationHandler = $this->getMockBuilder(TransformationHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $transformationHandler + ->expects($this->once()) + ->method('execute') + ->with($model) + ->will($this->returnValue('foobar')); + + $responseHandler = $this->getMockBuilder(ResponseHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $responseHandler + ->expects($this->once()) + ->method('execute') + ->with($response, $model); + + $postHandler = $this->getMockBuilder(PostHandlerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $postHandler + ->expects($this->once()) + ->method('execute') + ->with($model); + + $controller = $this->getMockBuilder(AbstractController::class) + ->setConstructorArgs([$model, $preHandler, $requestHandler, $queryHandler, $commandHandler, $transformationHandler, $responseHandler, $postHandler, $response]) + ->enableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->assertSame($response, $controller->processRequest($request)); + } + } +} diff --git a/Framework/tests/unit/Curl/CurlTest.php b/Framework/tests/unit/Curl/CurlTest.php new file mode 100644 index 0000000..4d6b7a2 --- /dev/null +++ b/Framework/tests/unit/Curl/CurlTest.php @@ -0,0 +1,135 @@ +getMockBuilder(CurlHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $credentials = $this->getMockBuilder(CredentialsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $uri = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $handler + ->expects($this->once()) + ->method('executeRequest') + ->with($uri, new Post, ['foo' => 'bar'], $credentials) + ->will($this->returnValue($response)); + + $curl = new Curl($handler); + + $this->assertEquals($response, $curl->post($uri, ['foo' => 'bar'], $credentials)); + } + + public function testGetWorks() + { + $handler = $this->getMockBuilder(CurlHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $credentials = $this->getMockBuilder(CredentialsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $uri = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $handler + ->expects($this->once()) + ->method('executeRequest') + ->with($uri, new Get, [], $credentials) + ->will($this->returnValue($response)); + + $curl = new Curl($handler); + + $this->assertEquals($response, $curl->get($uri, $credentials)); + } + + public function testDeleteWorks() + { + $handler = $this->getMockBuilder(CurlHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $credentials = $this->getMockBuilder(CredentialsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $uri = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $handler + ->expects($this->once()) + ->method('executeRequest') + ->with($uri, new Delete, [], $credentials) + ->will($this->returnValue($response)); + + $curl = new Curl($handler); + + $this->assertEquals($response, $curl->delete($uri, $credentials)); + } + + public function testHeadWorks() + { + $handler = $this->getMockBuilder(CurlHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $credentials = $this->getMockBuilder(CredentialsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $uri = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $handler + ->expects($this->once()) + ->method('executeRequest') + ->with($uri, new Head, [], $credentials) + ->will($this->returnValue($response)); + + $curl = new Curl($handler); + + $this->assertEquals($response, $curl->head($uri, $credentials)); + } + } +} diff --git a/Framework/tests/unit/Curl/RequestMethods/RequestMethodsTest.php b/Framework/tests/unit/Curl/RequestMethods/RequestMethodsTest.php new file mode 100644 index 0000000..cd02531 --- /dev/null +++ b/Framework/tests/unit/Curl/RequestMethods/RequestMethodsTest.php @@ -0,0 +1,41 @@ +assertEquals($method, (string) $requestMethod); + } + + public function provideData(): array + { + return [ + [new Get, 'GET'], + [new Post, 'POST'], + [new Delete, 'DELETE'], + [new Head, 'HEAD'], + [new Patch, 'PATCH'] + ]; + } + } +} diff --git a/Framework/tests/unit/Curl/ResponseTest.php b/Framework/tests/unit/Curl/ResponseTest.php new file mode 100644 index 0000000..3dc841e --- /dev/null +++ b/Framework/tests/unit/Curl/ResponseTest.php @@ -0,0 +1,27 @@ +assertEquals(404, $response->getCode()); + $this->assertEquals('not found', $response->getBody()); + } + + public function testJsonDecodeWorks() + { + $response = new Response(200, '{"message": "hello world"}'); + + $this->assertEquals(['message' => 'hello world'], $response->getJsonDecodedBody()); + } + } +} diff --git a/Framework/tests/unit/DataStore/RedisBackendTest.php b/Framework/tests/unit/DataStore/RedisBackendTest.php new file mode 100644 index 0000000..c41e030 --- /dev/null +++ b/Framework/tests/unit/DataStore/RedisBackendTest.php @@ -0,0 +1,122 @@ +redis = $this->getMockBuilder(\Redis::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->redisBackend = new RedisBackend($this->redis, '127.0.0.1', 6379); + } + + public function testSetWorks() + { + $this->redis + ->expects($this->once()) + ->method('isConnected') + ->will($this->returnValue(false)); + + $this->redis + ->expects($this->once()) + ->method('connect') + ->with('127.0.0.1', 6379); + + $this->redis + ->expects($this->once()) + ->method('set') + ->with('foo', 'bar'); + + $this->redisBackend->set('foo', 'bar'); + } + + public function testHasWorks() + { + $this->setUpIsConnected(); + + $this->redis + ->expects($this->once()) + ->method('exists') + ->with('foo') + ->will($this->returnValue(true)); + + $this->assertTrue($this->redisBackend->has('foo')); + } + + public function testGetWorks() + { + $this->setUpIsConnected(); + + $this->redis + ->expects($this->once()) + ->method('get') + ->with('answer') + ->will($this->returnValue(42)); + + $this->assertEquals(42, $this->redisBackend->get('answer')); + } + + public function testRemoveWorks() + { + $this->setUpIsConnected(); + + $this->redis + ->expects($this->once()) + ->method('delete') + ->with('foo'); + + $this->redisBackend->remove('foo'); + } + + public function testSetTimeoutWorks() + { + $this->setUpIsConnected(); + + $this->redis + ->expects($this->once()) + ->method('expire') + ->with('foo', 100); + + $this->redisBackend->setTimeout('foo', 100); + } + + public function testRemoveTimeoutWorks() + { + $this->setUpIsConnected(); + + $this->redis + ->expects($this->once()) + ->method('persist') + ->with('foo'); + + $this->redisBackend->removeTimeout('foo'); + } + + private function setUpIsConnected() + { + $this->redis + ->expects($this->once()) + ->method('isConnected') + ->will($this->returnValue(true)); + } + } +} diff --git a/Framework/tests/unit/ErrorHandlers/AbstractErrorHandlerTest.php b/Framework/tests/unit/ErrorHandlers/AbstractErrorHandlerTest.php new file mode 100644 index 0000000..5ba6e20 --- /dev/null +++ b/Framework/tests/unit/ErrorHandlers/AbstractErrorHandlerTest.php @@ -0,0 +1,29 @@ +getMockBuilder(AbstractErrorHandler::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + try { + $errorHandler->handleError(42, 'this is an error', '/dev/null', 25); + } catch (\ErrorException $exception) { + $this->assertEquals(-1, $exception->getCode()); + $this->assertEquals(42, $exception->getSeverity()); + $this->assertEquals('this is an error', $exception->getMessage()); + $this->assertEquals('/dev/null', $exception->getFile()); + $this->assertEquals(25, $exception->getLine()); + } + } + } +} diff --git a/Framework/tests/unit/Exceptions/FileUploadExceptionTest.php b/Framework/tests/unit/Exceptions/FileUploadExceptionTest.php new file mode 100644 index 0000000..2862348 --- /dev/null +++ b/Framework/tests/unit/Exceptions/FileUploadExceptionTest.php @@ -0,0 +1,38 @@ +assertEquals($code, $exception->getCode()); + $this->assertEquals($message, $exception->getMessage()); + } + + public function provideErrorCodes(): array + { + return [ + [UPLOAD_ERR_INI_SIZE, 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'], + [UPLOAD_ERR_FORM_SIZE, 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'], + [UPLOAD_ERR_PARTIAL, 'The uploaded file was only partially uploaded.'], + [UPLOAD_ERR_NO_FILE, 'No file was uploaded.'], + [UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder.'], + [UPLOAD_ERR_CANT_WRITE, 'Failed to write file to disk.'], + [UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the file upload.'], + [-1, 'An unknown error occurred'], + [99, 'An unknown error occurred'] + ]; + } + } +} diff --git a/Framework/tests/unit/Http/Redirect/AbstractRedirectTest.php b/Framework/tests/unit/Http/Redirect/AbstractRedirectTest.php new file mode 100644 index 0000000..bad9484 --- /dev/null +++ b/Framework/tests/unit/Http/Redirect/AbstractRedirectTest.php @@ -0,0 +1,28 @@ +getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $redirect = $this->getMockBuilder(AbstractRedirect::class) + ->setConstructorArgs([$uri]) + ->getMockForAbstractClass(); + + $this->assertSame($uri, $redirect->getUri()); + } + } +} diff --git a/Framework/tests/unit/Http/Redirect/PermanentRedirectTest.php b/Framework/tests/unit/Http/Redirect/PermanentRedirectTest.php new file mode 100644 index 0000000..bcb533a --- /dev/null +++ b/Framework/tests/unit/Http/Redirect/PermanentRedirectTest.php @@ -0,0 +1,27 @@ +getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $redirect = new PermanentRedirect($uri); + + $this->assertInstanceOf(MovedPermanently::class, $redirect->getStatusCode()); + } + } +} diff --git a/Framework/tests/unit/Http/Redirect/TemporaryRedirectTest.php b/Framework/tests/unit/Http/Redirect/TemporaryRedirectTest.php new file mode 100644 index 0000000..a2f3e78 --- /dev/null +++ b/Framework/tests/unit/Http/Redirect/TemporaryRedirectTest.php @@ -0,0 +1,27 @@ +getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $redirect = new TemporaryRedirect($uri); + + $this->assertInstanceOf(SeeOther::class, $redirect->getStatusCode()); + } + } +} diff --git a/Framework/tests/unit/Http/Request/PostRequestTest.php b/Framework/tests/unit/Http/Request/PostRequestTest.php new file mode 100644 index 0000000..f9d2d53 --- /dev/null +++ b/Framework/tests/unit/Http/Request/PostRequestTest.php @@ -0,0 +1,149 @@ +server = [ + 'HTTP_USER_AGENT' => 'curl/7.43.0', + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_AUTHORIZATION' => 'Bearer Foobar' + ]; + + $cookies = [ + 'foo' => 'bar' + ]; + + $params = [ + 'baz' => 'qux' + ]; + + $files = [ + 'file' => 'tata' + ]; + + $this->uri = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->request = new PostRequest($this->uri, $this->server, $cookies, $params, $files); + } + + public function testGetUriWorks() + { + $this->assertSame($this->uri, $this->request->getUri()); + } + + public function testGetUserAgentWorks() + { + $this->assertEquals($this->server['HTTP_USER_AGENT'], $this->request->getUserAgent()); + } + + public function testGetUserIpWorks() + { + $this->assertEquals($this->server['REMOTE_ADDR'], $this->request->getUserIp()); + } + + public function testHasCookieWorks() + { + $this->assertFalse($this->request->hasCookie('baz')); + $this->assertTrue($this->request->hasCookie('foo')); + } + + public function testGetCookieWorks() + { + $this->assertEquals('bar', $this->request->getCookie('foo')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage cookie with name "baz" not found + */ + public function testGetCookieThrowsForNonExisting() + { + $this->request->getCookie('baz'); + } + + public function testHasParamWorks() + { + $this->assertFalse($this->request->hasParam('sweg')); + $this->assertTrue($this->request->hasParam('baz')); + } + + public function testGetParamWorks() + { + $this->assertEquals('qux', $this->request->getParam('baz')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage param with name "foobar" was not found in request + */ + public function testGetParamThrowsForNonExisting() + { + $this->request->getParam('foobar'); + } + + public function testHasFileWorks() + { + $this->assertTrue($this->request->hasFile('file')); + $this->assertFalse($this->request->hasFile('foo')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage file with name "foo" was not found in request + */ + public function testGetFileThrowsForNonExisting() + { + $this->request->getFile('foo'); + } + + public function testGetAuthorizationWorks() + { + $auth = $this->request->getAuthorization(); + + $this->assertEquals($auth->getType(), 'Bearer'); + $this->assertEquals($auth->getToken(), 'Foobar'); + } + + public function testHasAuthorizationWorks() + { + $request = new PostRequest($this->uri, [], [], [], []); + + $this->assertFalse($request->hasAuthorization()); + $this->assertTrue($this->request->hasAuthorization()); + } + } +} diff --git a/Framework/tests/unit/Http/StatusCodes/StatusCodesTest.php b/Framework/tests/unit/Http/StatusCodes/StatusCodesTest.php new file mode 100644 index 0000000..c02145a --- /dev/null +++ b/Framework/tests/unit/Http/StatusCodes/StatusCodesTest.php @@ -0,0 +1,44 @@ +assertEquals($code, $statusCode->getCode()); + } + + public function provideData(): array + { + return [ + [new Created, 201], + [new InternalServerError, 500], + [new MovedPermanently, 301], + [new NotFound, 404], + [new SeeOther, 303], + [new BadRequest, 400], + [new MethodNotAllowed, 405], + [new Unauthorized, 401], + [new Forbidden, 403], + ]; + } + } +} diff --git a/Framework/tests/unit/Languages/LanguageTest.php b/Framework/tests/unit/Languages/LanguageTest.php new file mode 100644 index 0000000..a59cd92 --- /dev/null +++ b/Framework/tests/unit/Languages/LanguageTest.php @@ -0,0 +1,30 @@ +assertEquals((string) $language, $string); + } + + public function provideData(): array + { + return [ + [new \Timetabio\Framework\Languages\English, 'en_GB'], + [new \Timetabio\Framework\Languages\German, 'de_CH'] + ]; + } + } +} diff --git a/Framework/tests/unit/Logging/LoggerAwareTraitTest.php b/Framework/tests/unit/Logging/LoggerAwareTraitTest.php new file mode 100644 index 0000000..e7ab59c --- /dev/null +++ b/Framework/tests/unit/Logging/LoggerAwareTraitTest.php @@ -0,0 +1,34 @@ +getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + + $loggerAware = $this->getMockForTrait(LoggerAwareTrait::class); + + $closure = function () { + return $this->getLogger(); + }; + + $getLogger = $closure->bindTo($loggerAware, $loggerAware); + + $loggerAware->setLogger($logger); + + $this->assertSame($logger, $getLogger()); + } + } +} diff --git a/Framework/tests/unit/Logging/Loggers/LoggerTest.php b/Framework/tests/unit/Logging/Loggers/LoggerTest.php new file mode 100644 index 0000000..1ce827d --- /dev/null +++ b/Framework/tests/unit/Logging/Loggers/LoggerTest.php @@ -0,0 +1,55 @@ +logger = new Logger; + } + + public function testLogWorks() + { + $log = $this->getMockBuilder(AbstractLog::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $childLogger = $this->getMockBuilder(LoggerInterface::class) + ->getMockForAbstractClass(); + + $childLogger->expects($this->once()) + ->method('handles') + ->with($log) + ->will($this->returnValue(true)); + + $childLogger->expects($this->once()) + ->method('log') + ->with($log); + + $this->logger->addLogger($childLogger); + $this->logger->log($log); + } + + public function testHandlesWorks() + { + $log = $this->getMockBuilder(AbstractLog::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->assertTrue($this->logger->handles($log)); + } + } +} diff --git a/Framework/tests/unit/Logging/Loggers/SlackLoggerTest.php b/Framework/tests/unit/Logging/Loggers/SlackLoggerTest.php new file mode 100644 index 0000000..be6a5d6 --- /dev/null +++ b/Framework/tests/unit/Logging/Loggers/SlackLoggerTest.php @@ -0,0 +1,100 @@ +curl = $this->getMockBuilder(Curl::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->endpoint = $this->getMockBuilder(Uri::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->logger = new SlackLogger($this->curl, $this->endpoint); + } + + public function testLogWorks() + { + $log = $this->getMockBuilder(ErrorLog::class) + ->disableOriginalConstructor() + ->getMock(); + + $log->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue('foo')); + + $log->expects($this->once()) + ->method('getFile') + ->will($this->returnValue('foo.php')); + + $log->expects($this->once()) + ->method('getLine') + ->will($this->returnValue(25)); + + $log->expects($this->once()) + ->method('getStringTrace') + ->will($this->returnValue('TRACE')); + + $this->curl->expects($this->once()) + ->method('post') + ->with( + $this->endpoint, + ['payload' => '{"text":"*foo*\nin `foo.php:25`\n```TRACE```"}'] + ); + + $this->logger->log($log); + } + + public function testHandles() + { + $log = $this->getMockBuilder(EmergencyLog::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->assertTrue($this->logger->handles($log)); + } + + protected function getMockException(string $file, string $line) + { + $exception = $this->getMockBuilder(\Exception::class); + } + + protected function setProperty(\Exception $exception, string $property, $value) + { + $ref = new \ReflectionClass($exception); + + $refProperty = $ref->getProperty($property); + $refProperty->setAccessible(true); + $refProperty->setValue($exception, $value); + } + } +} diff --git a/Framework/tests/unit/Map/MapTest.php b/Framework/tests/unit/Map/MapTest.php new file mode 100644 index 0000000..40506ec --- /dev/null +++ b/Framework/tests/unit/Map/MapTest.php @@ -0,0 +1,70 @@ +map = new Map(['foo' => 'bar', 'baz' => 'qux']); + } + + public function testHasWorks() + { + $this->assertTrue($this->map->has('foo')); + $this->assertFalse($this->map->has('foo-bar')); + + $this->assertTrue($this->map->has('baz')); + $this->assertFalse($this->map->has('baz-qux')); + } + + public function testGetIteratorWorks() + { + $arr = []; + + foreach ($this->map as $key => $value) { + $arr[$key] = $value; + } + + $this->assertEquals($arr, ['foo' => 'bar', 'baz' => 'qux']); + } + + public function testGetWorks() + { + $this->assertEquals('bar', $this->map->get('foo')); + $this->assertEquals('qux', $this->map->get('baz')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage key "foo-bar" not found in map + */ + public function testGetThrowsIfKeyIsNotFound() + { + $this->map->get('foo-bar'); + } + + public function testSetWorks() + { + $this->map->set('a', 1); + $this->assertEquals(1, $this->map->get('a')); + } + + public function testRemoveWorks() + { + $this->assertTrue($this->map->has('foo')); + $this->map->remove('foo'); + $this->assertFalse($this->map->has('foo')); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/CookieTest.php b/Framework/tests/unit/ValueObjects/CookieTest.php new file mode 100644 index 0000000..665a890 --- /dev/null +++ b/Framework/tests/unit/ValueObjects/CookieTest.php @@ -0,0 +1,25 @@ +assertEquals('SESSID', $cookie->getName()); + $this->assertEquals('foobar', $cookie->getValue()); + $this->assertEquals('/foo', $cookie->getPath()); + $this->assertEquals(42, $cookie->getExpires()); + $this->assertEquals('test.timetab.io', $cookie->getDomain()); + $this->assertEquals(true, $cookie->isSecure()); + $this->assertEquals(false, $cookie->isHttpOnly()); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/EmailAddressTest.php b/Framework/tests/unit/ValueObjects/EmailAddressTest.php new file mode 100644 index 0000000..362d1f8 --- /dev/null +++ b/Framework/tests/unit/ValueObjects/EmailAddressTest.php @@ -0,0 +1,28 @@ +assertEquals('foo@example.com', (string) $email); + $this->assertEquals(json_encode('foo@example.com'), json_encode($email)); + } + + /** + * @expectedException \Exception + */ + public function testConstructorThrowsForInvalidEmail() + { + $email = new EmailAddress('foo@'); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/EmailPersonTest.php b/Framework/tests/unit/ValueObjects/EmailPersonTest.php new file mode 100644 index 0000000..3d6544f --- /dev/null +++ b/Framework/tests/unit/ValueObjects/EmailPersonTest.php @@ -0,0 +1,27 @@ +getMockBuilder(EmailAddress::class) + ->disableOriginalConstructor() + ->getMock(); + + $email->expects($this->exactly(2)) + ->method('__toString') + ->will($this->returnValue('root@example.com')); + + $this->assertEquals('Foo Bar ', (string) new EmailPerson($email, 'Foo Bar')); + $this->assertEquals('root@example.com', (string) new EmailPerson($email)); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/HeaderTest.php b/Framework/tests/unit/ValueObjects/HeaderTest.php new file mode 100644 index 0000000..3746970 --- /dev/null +++ b/Framework/tests/unit/ValueObjects/HeaderTest.php @@ -0,0 +1,27 @@ +assertEquals('content-type: text/plain', (string) $header); + } + + public function testGettersWork() + { + $header = new Header('content-type', 'text/plain'); + + $this->assertEquals('content-type', $header->getName()); + $this->assertEquals('text/plain', $header->getValue()); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/TokenTest.php b/Framework/tests/unit/ValueObjects/TokenTest.php new file mode 100644 index 0000000..eb108e2 --- /dev/null +++ b/Framework/tests/unit/ValueObjects/TokenTest.php @@ -0,0 +1,27 @@ +assertEquals('foobar', $token); + } + + public function testTokenGenerateWorks() + { + $token = new Token; + + $this->assertNotEmpty((string) $token); + $this->assertEquals(48, strlen($token)); + } + } +} diff --git a/Framework/tests/unit/ValueObjects/UriTest.php b/Framework/tests/unit/ValueObjects/UriTest.php new file mode 100644 index 0000000..942a1c0 --- /dev/null +++ b/Framework/tests/unit/ValueObjects/UriTest.php @@ -0,0 +1,65 @@ +assertEquals('https', $uri->getScheme()); + $this->assertEquals('google.com', $uri->getHost()); + $this->assertEquals('/foo/bar', $uri->getPath()); + $this->assertEquals(42, $uri->getQueryParam('answer')); + $this->assertTrue($uri->hasQueryParam('answer')); + $this->assertFalse($uri->hasQueryParam('foo')); + $this->assertEquals(['foo', 'bar'], $uri->getExplodedPath()); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage query param "foo" not found + */ + public function testGetQueryParameterThrows() + { + $uri = new Uri('https', 'google.com', '/foo/bar', ['answer' => 42]); + $uri->getQueryParam('foo'); + } + + /** + * @dataProvider provideUris + */ + public function testToStringWorks(Uri $uri, string $expected) + { + $this->assertEquals($expected, (string) $uri); + } + + public function provideUris(): array + { + return [ + [ + new Uri('https://google.com'), + 'https://google.com/' + ], + [ + new Uri('https://github.com/timetabio/timetab.io?a=10'), + 'https://github.com/timetabio/timetab.io?a=10' + ], + [ + new Uri('/timetabio/timetab.io'), + '/timetabio/timetab.io' + ], + [ + new Uri('/?answer=42'), + '/?answer=42' + ] + ]; + } + } +} diff --git a/Frontend/Rakefile b/Frontend/Rakefile new file mode 100644 index 0000000..8aef2f6 --- /dev/null +++ b/Frontend/Rakefile @@ -0,0 +1,18 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Frontend/bootstrap.php b/Frontend/bootstrap.php new file mode 100644 index 0000000..83a7cfc --- /dev/null +++ b/Frontend/bootstrap.php @@ -0,0 +1,4 @@ + + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + + ga('create', 'UA-88411585-1', 'auto'); + ga('send', 'pageview'); + diff --git a/Frontend/data/templates/content/verify/error.html b/Frontend/data/templates/content/verify/error.html new file mode 100644 index 0000000..463ae00 --- /dev/null +++ b/Frontend/data/templates/content/verify/error.html @@ -0,0 +1,17 @@ +
+
+ + + + + +

Not Found

+ +
+

+ This account has either already been verified or the provided token is invalid. +

+
+
+
+ diff --git a/Frontend/data/templates/content/verify/success.html b/Frontend/data/templates/content/verify/success.html new file mode 100644 index 0000000..5d40437 --- /dev/null +++ b/Frontend/data/templates/content/verify/success.html @@ -0,0 +1,23 @@ +
+
+ + + + + +

Account Verified

+ +
+

+ Congratulations! Your account has been verified. +

+
+ +
+

+ Go ahead and Sign In +

+
+
+
+ diff --git a/Frontend/data/templates/template.html b/Frontend/data/templates/template.html new file mode 100644 index 0000000..71e6053 --- /dev/null +++ b/Frontend/data/templates/template.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + diff --git a/Frontend/index.php b/Frontend/index.php new file mode 100644 index 0000000..93c494b --- /dev/null +++ b/Frontend/index.php @@ -0,0 +1,13 @@ +run(); +} diff --git a/Frontend/public/css b/Frontend/public/css new file mode 120000 index 0000000..829e054 --- /dev/null +++ b/Frontend/public/css @@ -0,0 +1 @@ +../../Styles/css \ No newline at end of file diff --git a/Frontend/public/favicon.ico b/Frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..597cc6a79ac9b5d9a935601ded35dd1c7c6ad791 GIT binary patch literal 90022 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 PH6liyH3WFtimetab.io \ No newline at end of file diff --git a/Frontend/public/images/mono-logo.svg b/Frontend/public/images/mono-logo.svg new file mode 100644 index 0000000..2062345 --- /dev/null +++ b/Frontend/public/images/mono-logo.svg @@ -0,0 +1,23 @@ + + + + LogoMonochrome + Created with Sketch. + + + + + + + diff --git a/Frontend/public/images/text-logo.svg b/Frontend/public/images/text-logo.svg new file mode 100644 index 0000000..5af764d --- /dev/null +++ b/Frontend/public/images/text-logo.svg @@ -0,0 +1,9 @@ + + + timetab.io + + diff --git a/Frontend/public/images/triangle.svg b/Frontend/public/images/triangle.svg new file mode 100644 index 0000000..f781739 --- /dev/null +++ b/Frontend/public/images/triangle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Frontend/public/js b/Frontend/public/js new file mode 120000 index 0000000..ec7b5e4 --- /dev/null +++ b/Frontend/public/js @@ -0,0 +1 @@ +../../Application/build \ No newline at end of file diff --git a/Frontend/scripts/add-versions.sh b/Frontend/scripts/add-versions.sh new file mode 100755 index 0000000..15121c7 --- /dev/null +++ b/Frontend/scripts/add-versions.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z ${VERSION} ]; then + echo "Refusing to build without VERSION" + exit +fi + +FILES=( + /js/polyfills.js + /js/application.js + /css/application.css +) + +TEMPLATE=$(cat) + +for FILE in ${FILES[@]}; do + EXTENSION="${FILE##*.}" + FILENAME="${FILE%.*}" + TEMPLATE=$(echo "${TEMPLATE}" | sed "s#${FILE}#${FILENAME}-${VERSION}.${EXTENSION}#g") +done + +echo "${TEMPLATE}" diff --git a/Frontend/src/Backends/ApiBackend.php b/Frontend/src/Backends/ApiBackend.php new file mode 100644 index 0000000..8563d57 --- /dev/null +++ b/Frontend/src/Backends/ApiBackend.php @@ -0,0 +1,61 @@ +curl = $curl; + $this->apiUrl = $apiUrl; + } + + public function post(string $endpoint, array $params, CredentialsInterface $credentials = null): ApiResponse + { + return new ApiResponse($this->curl->post($this->buildUrl($endpoint), $params, $credentials)); + } + + public function patch(string $endpoint, array $params, CredentialsInterface $credentials = null): ApiResponse + { + return new ApiResponse($this->curl->patch($this->buildUrl($endpoint), $params, $credentials)); + } + + public function get(string $endpoint, array $params, CredentialsInterface $credentials = null): ApiResponse + { + return new ApiResponse($this->curl->get($this->buildUrl($endpoint, $params), $credentials)); + } + + public function delete(string $endpoint, array $params, CredentialsInterface $credentials = null): ApiResponse + { + return new ApiResponse($this->curl->delete($this->buildUrl($endpoint, $params), $credentials)); + } + + private function buildUrl(string $endpoint, array $params = []): Uri + { + $query = ''; + + if (!empty($params)) { + $query = '?' . http_build_query($params); + } + + return new Uri($this->apiUrl . $endpoint . $query); + } + } +} diff --git a/Frontend/src/Bootstrap/Bootstrapper.php b/Frontend/src/Bootstrap/Bootstrapper.php new file mode 100644 index 0000000..a289058 --- /dev/null +++ b/Frontend/src/Bootstrap/Bootstrapper.php @@ -0,0 +1,110 @@ +bootstrapStreamWrappers(); + $this->bootstrapGettext(); + $this->bootstrapSession(); + } + + private function bootstrapSession() + { + /** @var Session $session */ + $session = $this->getFactory()->createSession(); + + $session->loadRequest($this->getRequest()); + } + + private function bootstrapStreamWrappers() + { + TemplatesStreamWrapper::setUp(__DIR__ . '/../../data/templates'); + } + + private function bootstrapGettext() + { + $gettext = new Gettext; + $gettext->setUp('messages', __DIR__ . '/../../../Locale'); + $gettext->setLanguage($this->getRequest()->getLanguage()); + } + + protected function buildConfiguration(): ConfigurationInterface + { + return new Configuration(__DIR__ . '/../../config/system.ini'); + } + + protected function buildFactory(): MasterFactoryInterface + { + $factory = new MasterFactory($this->getConfiguration()); + + $factory->registerFactory(new \Timetabio\Framework\Factories\FrameworkFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\BackendFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\LoggerFactory); + + $factory->registerFactory(new \Timetabio\Library\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\LocatorFactory); + + $factory->registerFactory(new \Timetabio\Frontend\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\ErrorHandlerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\ControllerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\HandlerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\RendererFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\TransformationFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\RouterFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\QueryFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\LocatorFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\SessionFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\CommandFactory); + + return $factory; + } + + protected function buildRouter(): RouterInterface + { + $router = new Router; + + // NOTE: The PageRouter always needs to be added earlier than the StaticPageRouter, + // because it overrides the / route from the static page renderer + $router->addRouter($this->getFactory()->createUserPageRouter()); + $router->addRouter($this->getFactory()->createStaticPageRouter()); + $router->addRouter($this->getFactory()->createPageRouter()); + $router->addRouter($this->getFactory()->createFeedPageRouter()); + $router->addRouter($this->getFactory()->createPostPageRouter()); + $router->addRouter($this->getFactory()->createUserActionRouter()); + $router->addRouter($this->getFactory()->createActionRouter()); + $router->addRouter($this->getFactory()->createUserFragmentRouter()); + $router->addRouter($this->getFactory()->createFragmentRouter()); + + $router->addRouter($this->getFactory()->createNotFoundRouter()); + + return $router; + } + + protected function buildErrorHandler(): AbstractErrorHandler + { + if ($this->getConfiguration()->isDevelopmentMode()) { + return $this->getFactory()->createDevelopmentErrorHandler(); + } + + return $this->getFactory()->createProductionErrorHandler(); + } + } +} diff --git a/Frontend/src/Commands/AbstractApiCommand.php b/Frontend/src/Commands/AbstractApiCommand.php new file mode 100644 index 0000000..6df8df1 --- /dev/null +++ b/Frontend/src/Commands/AbstractApiCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + protected function getApiGateway(): ApiGateway + { + return $this->apiGateway; + } + } +} diff --git a/Frontend/src/Commands/CreateBetaRequestCommand.php b/Frontend/src/Commands/CreateBetaRequestCommand.php new file mode 100644 index 0000000..5c4df98 --- /dev/null +++ b/Frontend/src/Commands/CreateBetaRequestCommand.php @@ -0,0 +1,14 @@ +getApiGateway()->createBetaRequest($email)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/CreateFeedCommand.php b/Frontend/src/Commands/CreateFeedCommand.php new file mode 100644 index 0000000..2a715a8 --- /dev/null +++ b/Frontend/src/Commands/CreateFeedCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $name, string $description, bool $isPrivate): array + { + return $this->apiGateway->createFeed($name, $description, $isPrivate)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/CreateNoteCommand.php b/Frontend/src/Commands/CreateNoteCommand.php new file mode 100644 index 0000000..883d4a3 --- /dev/null +++ b/Frontend/src/Commands/CreateNoteCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId, string $title, string $body, array $attachments): array + { + return $this->apiGateway->createNote($feedId, $title, $body, $attachments)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/CreateUploadCommand.php b/Frontend/src/Commands/CreateUploadCommand.php new file mode 100644 index 0000000..733f7ce --- /dev/null +++ b/Frontend/src/Commands/CreateUploadCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $filename, string $fileType): array + { + return $this->apiGateway->createUpload($filename, $fileType)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/DeleteFeedUserCommand.php b/Frontend/src/Commands/DeleteFeedUserCommand.php new file mode 100644 index 0000000..c7beb24 --- /dev/null +++ b/Frontend/src/Commands/DeleteFeedUserCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId, string $userId) + { + return $this->apiGateway->deleteFeedUser($feedId, $userId)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/DeletePostCommand.php b/Frontend/src/Commands/DeletePostCommand.php new file mode 100644 index 0000000..87f51ff --- /dev/null +++ b/Frontend/src/Commands/DeletePostCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $postId) + { + return $this->apiGateway->deletePost($postId)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/Feed/DeleteFeedInvitationCommand.php b/Frontend/src/Commands/Feed/DeleteFeedInvitationCommand.php new file mode 100644 index 0000000..f2ccac7 --- /dev/null +++ b/Frontend/src/Commands/Feed/DeleteFeedInvitationCommand.php @@ -0,0 +1,16 @@ +getApiGateway()->deleteFeedInvitation($feedId, $userId)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/Feed/FollowFeedCommand.php b/Frontend/src/Commands/Feed/FollowFeedCommand.php new file mode 100644 index 0000000..c516a98 --- /dev/null +++ b/Frontend/src/Commands/Feed/FollowFeedCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId) + { + return $this->apiGateway->followFeed($feedId)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/Feed/UnfollowFeedCommand.php b/Frontend/src/Commands/Feed/UnfollowFeedCommand.php new file mode 100644 index 0000000..0798538 --- /dev/null +++ b/Frontend/src/Commands/Feed/UnfollowFeedCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId) + { + return $this->apiGateway->unfollowFeed($feedId)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/Feed/UpdateFeedUserRoleCommand.php b/Frontend/src/Commands/Feed/UpdateFeedUserRoleCommand.php new file mode 100644 index 0000000..4e48713 --- /dev/null +++ b/Frontend/src/Commands/Feed/UpdateFeedUserRoleCommand.php @@ -0,0 +1,16 @@ +getApiGateway()->updateFeedUser($feedId, $userId, $role)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/InviteFeedUserCommand.php b/Frontend/src/Commands/InviteFeedUserCommand.php new file mode 100644 index 0000000..e98cf91 --- /dev/null +++ b/Frontend/src/Commands/InviteFeedUserCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId, string $username, string $role) + { + return $this->apiGateway->inviteFeedUser($feedId, $username, $role)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/LoginCommand.php b/Frontend/src/Commands/LoginCommand.php new file mode 100644 index 0000000..1e2f162 --- /dev/null +++ b/Frontend/src/Commands/LoginCommand.php @@ -0,0 +1,45 @@ +apiGateway = $apiGateway; + $this->session = $session; + } + + public function execute(string $user, string $password) + { + $response = $this->apiGateway->authenticate($user, $password); + $authData = $response->unwrap(); + + $this->session->setAccessToken($authData['access_token']); + + $userInfo = $this->apiGateway->getUser()->unwrap(); + + $this->session->setUser(new User( + $userInfo['id'], + $userInfo['username'], + $userInfo['name'] + )); + } + } +} diff --git a/Frontend/src/Commands/LogoutCommand.php b/Frontend/src/Commands/LogoutCommand.php new file mode 100644 index 0000000..498c612 --- /dev/null +++ b/Frontend/src/Commands/LogoutCommand.php @@ -0,0 +1,38 @@ +session = $session; + $this->apiGateway = $apiGateway; + } + + public function execute() + { + $token = $this->session->getAccessToken(); + + $this->session->removeUser(); + $this->session->removeAccessToken(); + + $this->apiGateway->revokeToken($token); + } + } +} diff --git a/Frontend/src/Commands/RegisterCommand.php b/Frontend/src/Commands/RegisterCommand.php new file mode 100644 index 0000000..35eee69 --- /dev/null +++ b/Frontend/src/Commands/RegisterCommand.php @@ -0,0 +1,32 @@ +apiGateway = $apiGateway; + } + + public function execute(string $email, string $username, string $password) + { + $user = $this->apiGateway->createUser($email, $username, $password)->unwrap(); + + if ($user === null) { + throw new \RuntimeException('invalid api response'); + } + + return $user; + } + } +} diff --git a/Frontend/src/Commands/ResendVerificationCommand.php b/Frontend/src/Commands/ResendVerificationCommand.php new file mode 100644 index 0000000..304ff74 --- /dev/null +++ b/Frontend/src/Commands/ResendVerificationCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $email) + { + return $this->apiGateway->resendVerification($email)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/VerifyCommand.php b/Frontend/src/Commands/VerifyCommand.php new file mode 100644 index 0000000..6314e94 --- /dev/null +++ b/Frontend/src/Commands/VerifyCommand.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $token) + { + return $this->apiGateway->verifyUser($token)->unwrap(); + } + } +} diff --git a/Frontend/src/Commands/WriteSessionCommand.php b/Frontend/src/Commands/WriteSessionCommand.php new file mode 100644 index 0000000..e89a6bf --- /dev/null +++ b/Frontend/src/Commands/WriteSessionCommand.php @@ -0,0 +1,28 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function execute(Session $session) + { + $this->dataStoreWriter->setSessionData($session->getSessionId(), $session->getData()); + $this->dataStoreWriter->expireSessionData($session->getSessionId(), $session->getExpires()); + } + } +} diff --git a/Frontend/src/DataObjects/ApiResponse.php b/Frontend/src/DataObjects/ApiResponse.php new file mode 100644 index 0000000..72a16a4 --- /dev/null +++ b/Frontend/src/DataObjects/ApiResponse.php @@ -0,0 +1,41 @@ +response = $response; + } + + /** + * @throws ApiException + */ + public function unwrap() + { + $code = $this->response->getCode(); + $data = $this->response->getJsonDecodedBody(); + + if ($code === 404) { + return null; + } + + if ($code >= 400 && $code < 600) { + throw new ApiException($data['error']); + } + + return $data; + } + } +} diff --git a/Frontend/src/DataObjects/User.php b/Frontend/src/DataObjects/User.php new file mode 100644 index 0000000..d0fa0b4 --- /dev/null +++ b/Frontend/src/DataObjects/User.php @@ -0,0 +1,56 @@ +userId = $userId; + $this->username = $username; + $this->name = $name; + } + + public function getUserId(): string + { + return $this->userId; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getName() + { + return $this->name; + } + + public function getDisplayName(): string + { + return new DisplayName([ + 'name' => $this->name, + 'username' => $this->username + ]); + } + } +} diff --git a/Frontend/src/DataStore/DataStoreReader.php b/Frontend/src/DataStore/DataStoreReader.php new file mode 100644 index 0000000..0c76f00 --- /dev/null +++ b/Frontend/src/DataStore/DataStoreReader.php @@ -0,0 +1,44 @@ +getDataStore()->get('system_token'); + } + + public function hasSessionData(string $sessionId) + { + return $this->getDataStore()->has('session_data_' . $sessionId); + } + + public function getSessionData(string $sessionId): MapInterface + { + return unserialize($this->getDataStore()->get('session_data_' . $sessionId)); + } + + public function getStaticPage(string $name, LanguageInterface $language): StaticPage + { + return unserialize($this->getDataStore()->getFromHash('static_pages_' . $language, $name)); + } + + public function hasRoute(string $path) + { + return $this->getDataStore()->hasInHash('static_routes', $path); + } + + public function getRoute(string $path): string + { + return $this->getDataStore()->getFromHash('static_routes', $path); + } + } +} diff --git a/Frontend/src/DataStore/DataStoreWriter.php b/Frontend/src/DataStore/DataStoreWriter.php new file mode 100644 index 0000000..6004e77 --- /dev/null +++ b/Frontend/src/DataStore/DataStoreWriter.php @@ -0,0 +1,32 @@ +dataStore = $dataStore; + } + + public function setSessionData(string $sessionId, MapInterface $sessionData) + { + $this->dataStore->set('session_data_' . $sessionId, serialize($sessionData)); + } + + public function expireSessionData(string $sessionId, int $ttl) + { + $this->dataStore->setTimeout('session_data_' . $sessionId, $ttl); + } + } +} diff --git a/Frontend/src/ErrorHandlers/DevelopmentErrorHandler.php b/Frontend/src/ErrorHandlers/DevelopmentErrorHandler.php new file mode 100644 index 0000000..773ca6f --- /dev/null +++ b/Frontend/src/ErrorHandlers/DevelopmentErrorHandler.php @@ -0,0 +1,51 @@ +handleAbstractException($exception); + return; + } + + $this->handleFatalException($exception); + } + + private function handleAbstractException(AbstractException $exception) + { + http_response_code($exception->getStatusCode()->getCode()); + + header('content-type: application/json'); + + echo json_encode([ + 'error' => $exception->getMessage() + ]); + } + + private function handleFatalException(\Throwable $exception) + { + http_response_code(500); + + echo '
'; + echo '

' . htmlentities($exception->getMessage()) . '

'; + echo 'File: ' . htmlentities($exception->getFile()) . '
'; + echo 'Line: ' . $exception->getLine(); + + echo '
' . htmlentities($exception->getTraceAsString()) . '
'; + echo '
'; + + echo ''; + echo ''; + + die(); + } + } +} diff --git a/Frontend/src/ErrorHandlers/ProductionErrorHandler.php b/Frontend/src/ErrorHandlers/ProductionErrorHandler.php new file mode 100644 index 0000000..1bb2de1 --- /dev/null +++ b/Frontend/src/ErrorHandlers/ProductionErrorHandler.php @@ -0,0 +1,40 @@ +handleAbstractException($exception); + return; + } + + $this->getLogger()->error($exception); + + header('Location: /error'); + exit; + } + + private function handleAbstractException(AbstractException $exception) + { + http_response_code($exception->getStatusCode()->getCode()); + + header('content-type: application/json'); + + echo json_encode([ + 'error' => $exception->getMessage() + ]); + } + } +} diff --git a/Frontend/src/Exceptions/AbstractException.php b/Frontend/src/Exceptions/AbstractException.php new file mode 100644 index 0000000..dd2dfb6 --- /dev/null +++ b/Frontend/src/Exceptions/AbstractException.php @@ -0,0 +1,13 @@ +getMessage(); + } + } +} diff --git a/Frontend/src/Exceptions/BadRequest.php b/Frontend/src/Exceptions/BadRequest.php new file mode 100644 index 0000000..3b5e43e --- /dev/null +++ b/Frontend/src/Exceptions/BadRequest.php @@ -0,0 +1,16 @@ +getMasterFactory()->createRedisBackend() + ); + } + + public function createDataStoreWriter(): \Timetabio\Frontend\DataStore\DataStoreWriter + { + return new \Timetabio\Frontend\DataStore\DataStoreWriter( + $this->getMasterFactory()->createRedisBackend() + ); + } + + public function createApiBackend(): \Timetabio\Frontend\Backends\ApiBackend + { + return new \Timetabio\Frontend\Backends\ApiBackend( + $this->getMasterFactory()->createCurl(), + $this->getMasterFactory()->getConfiguration()->get('apiUrl') + ); + } + + public function createApiGateway(): \Timetabio\Frontend\Gateways\ApiGateway + { + return new \Timetabio\Frontend\Gateways\ApiGateway( + $this->getMasterFactory()->createApiBackend(), + $this->getMasterFactory()->createSession(), + new BearerToken($this->getMasterFactory()->createDataStoreReader()->getSystemToken()) + ); + } + } +} diff --git a/Frontend/src/Factories/CommandFactory.php b/Frontend/src/Factories/CommandFactory.php new file mode 100644 index 0000000..6f68e3c --- /dev/null +++ b/Frontend/src/Factories/CommandFactory.php @@ -0,0 +1,135 @@ +getMasterFactory()->createDataStoreWriter() + ); + } + + public function createLoginCommand(): \Timetabio\Frontend\Commands\LoginCommand + { + return new \Timetabio\Frontend\Commands\LoginCommand( + $this->getMasterFactory()->createApiGateway(), + $this->getMasterFactory()->createSession() + ); + } + + public function createCreateFeedCommand(): \Timetabio\Frontend\Commands\CreateFeedCommand + { + return new \Timetabio\Frontend\Commands\CreateFeedCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createCreateNoteCommand(): \Timetabio\Frontend\Commands\CreateNoteCommand + { + return new \Timetabio\Frontend\Commands\CreateNoteCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createResendVerificationCommand(): \Timetabio\Frontend\Commands\ResendVerificationCommand + { + return new \Timetabio\Frontend\Commands\ResendVerificationCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createLogoutCommand(): \Timetabio\Frontend\Commands\LogoutCommand + { + return new \Timetabio\Frontend\Commands\LogoutCommand( + $this->getMasterFactory()->createSession(), + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFollowFeedCommand(): \Timetabio\Frontend\Commands\Feed\FollowFeedCommand + { + return new \Timetabio\Frontend\Commands\Feed\FollowFeedCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createUnfollowFeedCommand(): \Timetabio\Frontend\Commands\Feed\UnfollowFeedCommand + { + return new \Timetabio\Frontend\Commands\Feed\UnfollowFeedCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createRegisterCommand(): \Timetabio\Frontend\Commands\RegisterCommand + { + return new \Timetabio\Frontend\Commands\RegisterCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createVerifyCommand(): \Timetabio\Frontend\Commands\VerifyCommand + { + return new \Timetabio\Frontend\Commands\VerifyCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createDeletePostCommand(): \Timetabio\Frontend\Commands\DeletePostCommand + { + return new \Timetabio\Frontend\Commands\DeletePostCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createCreateUploadCommand(): \Timetabio\Frontend\Commands\CreateUploadCommand + { + return new \Timetabio\Frontend\Commands\CreateUploadCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createCreateBetaRequestCommand(): \Timetabio\Frontend\Commands\CreateBetaRequestCommand + { + return new \Timetabio\Frontend\Commands\CreateBetaRequestCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createDeleteFeedUserCommand(): \Timetabio\Frontend\Commands\DeleteFeedUserCommand + { + return new \Timetabio\Frontend\Commands\DeleteFeedUserCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createInviteFeedUserCommand(): \Timetabio\Frontend\Commands\InviteFeedUserCommand + { + return new \Timetabio\Frontend\Commands\InviteFeedUserCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createDeleteFeedInvitationCommand(): Commands\Feed\DeleteFeedInvitationCommand + { + return new Commands\Feed\DeleteFeedInvitationCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createUpdateFeedUserRoleCommand(): Commands\Feed\UpdateFeedUserRoleCommand + { + return new Commands\Feed\UpdateFeedUserRoleCommand( + $this->getMasterFactory()->createApiGateway() + ); + } + } +} diff --git a/Frontend/src/Factories/ControllerFactory.php b/Frontend/src/Factories/ControllerFactory.php new file mode 100644 index 0000000..fcb5e58 --- /dev/null +++ b/Frontend/src/Factories/ControllerFactory.php @@ -0,0 +1,412 @@ +getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetStaticPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createRegisterController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\RegisterModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createPostRegisterRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createPostRegisterCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createVerifyAccountController(): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Account\VerifyModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createGetVerifyAccountRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createGetVerifyAccountCommandHandler(), + $this->getMasterFactory()->createGetVerifyAccountTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createLoginController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\LoginModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createLoginRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createLoginCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createResendVerificationController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\ResendVerificationModel(), + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createResendVerificationRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createResendVerificationCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createLogoutController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\ActionModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createLogoutCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createHomepageController(): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\HomepageModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetHomepageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetHomepageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createFeedsPageController(): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\FeedsPageModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createFeedsPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createFeedsPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createNewFeedController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Account\NewFeedModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createNewFeedRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createNewFeedCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetFeedPageController(array $feed): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Page\FeedPostsPageModel( + new \Timetabio\Frontend\ValueObjects\Feed($feed), + new \Timetabio\Frontend\Tabs\FeedPage\Posts + ), + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetFeedPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetFeedPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetCreatePostPageController(array $feed): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\CreatePostPageModel($feed), + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetCreatePostPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetCreatePostPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createCreateNoteController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\CreateNoteModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createCreateNoteRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateNoteCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createFollowController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\FollowModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createFollowRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createFollowCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createUnfollowController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\FollowModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createFollowRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createUnfollowCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createDeletePostController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\DeletePostModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createDeletePostRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createDeletePostCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createCreateUploadController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\UploadModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createCreateUploadRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateUploadCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetPostPageController(array $post): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\PostPageModel($post), + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createGetPostPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetPostPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createCreateBetaRequestController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\CreateBetaRequestModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createCreateBetaRequestRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createCreateBetaRequestCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createSearchPageController(SearchType $type): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Page\SearchPageModel($type), + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createSearchPageRequestHandler(), + $this->getMasterFactory()->createSearchPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createSearchPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetFeedPostsFragmentController(): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Fragment\FeedPostsFragmentModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createGetFeedPostsFragmentRequestHandler(), + $this->getMasterFactory()->createGetFeedPostsFragmentQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetFeedPostsFragmentTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetHomepagePostsFragmentController(): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Fragment\HomepagePostsFragmentModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createGetHomepagePostsFragmentRequestHandler(), + $this->getMasterFactory()->createGetHomepagePostsFragmentQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetHomepagePostsFragmentTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createGetFeedPeoplePageController(array $feed): \Timetabio\Framework\Controllers\GetController + { + return new \Timetabio\Framework\Controllers\GetController( + new \Timetabio\Frontend\Models\Page\FeedPeoplePageModel( + new \Timetabio\Frontend\ValueObjects\Feed($feed), + new \Timetabio\Frontend\Tabs\FeedPage\People + ), + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createGetFeedPeoplePageRequestHandler(), + $this->getMasterFactory()->createGetFeedPeoplePageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createGetFeedPeoplePageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createDeleteFeedUserController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\DeleteFeedUserModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createDeleteFeedUserRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createDeleteFeedUserCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createInviteFeedUserController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\InviteFeedUserModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createInviteFeedUserRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createInviteFeedUserCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createDeleteFeedInvitationController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\DeleteFeedUserModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createDeleteFeedInvitationRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createDeleteFeedInvitationCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createUpdateFeedUserRoleController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Frontend\Models\Action\UpdateFeedUserRoleModel, + $this->getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createUpdateFeedUserRoleRequestHandler(), + $this->getMasterFactory()->createQueryHandler(), + $this->getMasterFactory()->createUpdateFeedUserRoleCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + } +} diff --git a/Frontend/src/Factories/ErrorHandlerFactory.php b/Frontend/src/Factories/ErrorHandlerFactory.php new file mode 100644 index 0000000..df76105 --- /dev/null +++ b/Frontend/src/Factories/ErrorHandlerFactory.php @@ -0,0 +1,21 @@ +getMasterFactory()->createSession(), + $this->getMasterFactory()->createWriteSessionCommand() + ); + } + + public function createPreHandler(): \Timetabio\Frontend\Handlers\PreHandler + { + return new \Timetabio\Frontend\Handlers\PreHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createQueryHandler(): \Timetabio\Frontend\Handlers\QueryHandler + { + return new \Timetabio\Frontend\Handlers\QueryHandler; + } + + public function createResponseHandler(): \Timetabio\Frontend\Handlers\ResponseHandler + { + return new \Timetabio\Frontend\Handlers\ResponseHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createGetPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createStaticPageRenderer() + ); + } + + public function createGetPagePreHandler(): \Timetabio\Frontend\Handlers\Get\Page\PreHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\PreHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createGetStaticPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\StaticPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\StaticPage\QueryHandler( + $this->getMasterFactory()->createFetchStaticPageQuery() + ); + } + + public function createPostTransformationHandler(): \Timetabio\Frontend\Handlers\Post\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Post\TransformationHandler; + } + + public function createPostPreHandler(): \Timetabio\Frontend\Handlers\Post\PreHandler + { + return new \Timetabio\Frontend\Handlers\Post\PreHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createPostRegisterRequestHandler(): \Timetabio\Frontend\Handlers\Post\Register\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\Register\RequestHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createPostRegisterCommandHandler(): \Timetabio\Frontend\Handlers\Post\Register\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Register\CommandHandler( + $this->getMasterFactory()->createRegisterCommand() + ); + } + + public function createLoginRequestHandler(): \Timetabio\Frontend\Handlers\Post\Login\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\Login\RequestHandler( + $this->getMasterFactory()->createSession() + ); + } + + public function createResendVerificationRequestHandler(): \Timetabio\Frontend\Handlers\Post\ResendVerification\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\ResendVerification\RequestHandler( + $this->getMasterFactory()->createSession()); + } + + public function createResendVerificationCommandHandler(): \Timetabio\Frontend\Handlers\Post\ResendVerification\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\ResendVerification\CommandHandler( + $this->getMasterFactory()->createResendVerificationCommand() + ); + } + + public function createLoginCommandHandler(): \Timetabio\Frontend\Handlers\Post\Login\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Login\CommandHandler( + $this->getMasterFactory()->createLoginCommand() + ); + } + + public function createLogoutCommandHandler(): \Timetabio\Frontend\Handlers\Post\Logout\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Logout\CommandHandler( + $this->getMasterFactory()->createLogoutCommand() + ); + } + + public function createGetVerifyAccountRequestHandler(): \Timetabio\Frontend\Handlers\Get\Account\Verify\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Get\Account\Verify\RequestHandler; + } + + public function createGetVerifyAccountCommandHandler(): \Timetabio\Frontend\Handlers\Get\Account\Verify\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Get\Account\Verify\CommandHandler( + $this->getMasterFactory()->createVerifyCommand() + ); + } + + public function createGetVerifyAccountTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createVerifyAccountPageRenderer() + ); + } + + public function createGetHomepageQueryHandler(): \Timetabio\Frontend\Handlers\Get\Homepage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\Homepage\QueryHandler( + $this->getMasterFactory()->createFetchUserFeedQuery() + ); + } + + public function createGetHomepageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createHomepageRenderer() + ); + } + + public function createFeedsPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\FeedsPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedsPage\QueryHandler( + $this->getMasterFactory()->createFetchUserFeedsQuery() + ); + } + + public function createFeedsPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createFeedsPageRenderer() + ); + } + + public function createNewFeedCommandHandler(): \Timetabio\Frontend\Handlers\Post\NewFeed\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\NewFeed\CommandHandler( + $this->getMasterFactory()->createCreateFeedCommand(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createNewFeedRequestHandler(): \Timetabio\Frontend\Handlers\Post\NewFeed\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\NewFeed\RequestHandler; + } + + public function createGetFeedPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\FeedPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedPage\QueryHandler( + $this->getMasterFactory()->createFetchFeedPostsQuery(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createGetFeedPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createFeedPageRenderer() + ); + } + + public function createGetCreatePostPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\CreatePostPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\CreatePostPage\QueryHandler; + } + + public function createGetCreatePostPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createCreatePostPageRenderer() + ); + } + + public function createCreateNoteCommandHandler(): \Timetabio\Frontend\Handlers\Post\CreateNote\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\CreateNote\CommandHandler( + $this->getMasterFactory()->createCreateNoteCommand(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createCreateNoteRequestHandler(): \Timetabio\Frontend\Handlers\Post\CreateNote\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\CreateNote\RequestHandler; + } + + public function createFollowRequestHandler(): \Timetabio\Frontend\Handlers\Post\Follow\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\Follow\RequestHandler; + } + + public function createFollowCommandHandler(): \Timetabio\Frontend\Handlers\Post\Follow\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Follow\CommandHandler( + $this->getMasterFactory()->createFollowFeedCommand() + ); + } + + public function createUnfollowCommandHandler(): \Timetabio\Frontend\Handlers\Post\Unfollow\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Unfollow\CommandHandler( + $this->getMasterFactory()->createUnfollowFeedCommand() + ); + } + + public function createDeletePostRequestHandler(): \Timetabio\Frontend\Handlers\Post\DeletePost\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeletePost\RequestHandler; + } + + public function createDeletePostCommandHandler(): \Timetabio\Frontend\Handlers\Post\DeletePost\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeletePost\CommandHandler( + $this->getMasterFactory()->createDeletePostCommand(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createCreateUploadRequestHandler(): \Timetabio\Frontend\Handlers\Post\Upload\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\Upload\RequestHandler; + } + + public function createCreateUploadCommandHandler(): \Timetabio\Frontend\Handlers\Post\Upload\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\Upload\CommandHandler( + $this->getMasterFactory()->createCreateUploadCommand() + ); + } + + public function createGetPostPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\PostPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\PostPage\QueryHandler( + $this->getMasterFactory()->createFetchFeedPostsQuery() + ); + } + + public function createGetPostPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createPostPageRenderer() + ); + } + + public function createCreateBetaRequestCommandHandler(): \Timetabio\Frontend\Handlers\Post\CreateBetaRequest\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\CreateBetaRequest\CommandHandler( + $this->getMasterFactory()->createCreateBetaRequestCommand() + ); + } + + public function createCreateBetaRequestRequestHandler(): \Timetabio\Frontend\Handlers\Post\CreateBetaRequest\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\CreateBetaRequest\RequestHandler; + } + + public function createSearchPageRequestHandler(): \Timetabio\Frontend\Handlers\Get\SearchPage\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Get\SearchPage\RequestHandler; + } + + public function createSearchPageQueryHandler(): \Timetabio\Frontend\Handlers\Get\SearchPage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\SearchPage\QueryHandler( + $this->getMasterFactory()->createSearchQuery(), + $this->getMasterFactory()->createSearchTabLocator() + ); + } + + public function createSearchPageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createSearchPageRenderer() + ); + } + + public function createGetFeedPostsFragmentQueryHandler(): \Timetabio\Frontend\Handlers\Get\FeedPostsFragment\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedPostsFragment\QueryHandler( + $this->getMasterFactory()->createFetchFeedQuery(), + $this->getMasterFactory()->createFetchFeedPostsQuery() + ); + } + + public function createGetFeedPostsFragmentRequestHandler(): \Timetabio\Frontend\Handlers\Get\FeedPostsFragment\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedPostsFragment\RequestHandler; + } + + public function createGetFeedPostsFragmentTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Fragment\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Fragment\TransformationHandler( + $this->getMasterFactory()->createFeedPostsFragmentRenderer() + ); + } + + public function createGetHomepagePostsFragmentQueryHandler(): \Timetabio\Frontend\Handlers\Get\HomepagePostsFragment\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\HomepagePostsFragment\QueryHandler( + $this->getMasterFactory()->createFetchUserFeedQuery() + ); + } + + public function createGetHomepagePostsFragmentRequestHandler(): \Timetabio\Frontend\Handlers\Get\HomepagePostsFragment\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Get\HomepagePostsFragment\RequestHandler; + } + + public function createGetHomepagePostsFragmentTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Fragment\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Fragment\TransformationHandler( + $this->getMasterFactory()->createHomepagePostsFragmentRenderer() + ); + } + + public function createGetFeedPeoplePageQueryHandler(): \Timetabio\Frontend\Handlers\Get\FeedPeoplePage\QueryHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedPeoplePage\QueryHandler( + $this->getMasterFactory()->createFetchFeedUsersQuery(), + $this->getMasterFactory()->createFetchFeedInvitationsQuery() + ); + } + + public function createGetFeedPeoplePageRequestHandler(): \Timetabio\Frontend\Handlers\Get\FeedPeoplePage\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Get\FeedPeoplePage\RequestHandler; + } + + public function createGetFeedPeoplePageTransformationHandler(): \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler + { + return new \Timetabio\Frontend\Handlers\Get\Page\TransformationHandler( + $this->getMasterFactory()->createFeedPeoplePageRenderer() + ); + } + + public function createDeleteFeedUserCommandHandler(): \Timetabio\Frontend\Handlers\Post\DeleteFeedUser\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeleteFeedUser\CommandHandler( + $this->getMasterFactory()->createDeleteFeedUserCommand() + ); + } + + public function createDeleteFeedUserRequestHandler(): \Timetabio\Frontend\Handlers\Post\DeleteFeedUser\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeleteFeedUser\RequestHandler; + } + + public function createInviteFeedUserCommandHandler(): \Timetabio\Frontend\Handlers\Post\InviteFeedUser\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\InviteFeedUser\CommandHandler( + $this->getMasterFactory()->createInviteFeedUserCommand() + ); + } + + public function createInviteFeedUserRequestHandler(): \Timetabio\Frontend\Handlers\Post\InviteFeedUser\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\InviteFeedUser\RequestHandler; + } + + public function createDeleteFeedInvitationCommandHandler(): \Timetabio\Frontend\Handlers\Post\DeleteFeedInvitation\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeleteFeedInvitation\CommandHandler( + $this->getMasterFactory()->createDeleteFeedInvitationCommand() + ); + } + + public function createDeleteFeedInvitationRequestHandler(): \Timetabio\Frontend\Handlers\Post\DeleteFeedInvitation\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\DeleteFeedInvitation\RequestHandler; + } + + public function createUpdateFeedUserRoleCommandHandler(): \Timetabio\Frontend\Handlers\Post\UpdateFeedUserRole\CommandHandler + { + return new \Timetabio\Frontend\Handlers\Post\UpdateFeedUserRole\CommandHandler( + $this->getMasterFactory()->createUpdateFeedUserRoleCommand() + ); + } + + public function createUpdateFeedUserRoleRequestHandler(): \Timetabio\Frontend\Handlers\Post\UpdateFeedUserRole\RequestHandler + { + return new \Timetabio\Frontend\Handlers\Post\UpdateFeedUserRole\RequestHandler; + } + } +} diff --git a/Frontend/src/Factories/LocatorFactory.php b/Frontend/src/Factories/LocatorFactory.php new file mode 100644 index 0000000..014377a --- /dev/null +++ b/Frontend/src/Factories/LocatorFactory.php @@ -0,0 +1,23 @@ +getMasterFactory()->createDataStoreReader() + ); + } + + public function createIsLoggedInQuery(): \Timetabio\Frontend\Queries\IsLoggedInQuery + { + return new \Timetabio\Frontend\Queries\IsLoggedInQuery( + $this->getMasterFactory()->createSession() + ); + } + + public function createFetchUserFeedsQuery(): \Timetabio\Frontend\Queries\FetchUserFeedsQuery + { + return new \Timetabio\Frontend\Queries\FetchUserFeedsQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFetchFeedQuery(): \Timetabio\Frontend\Queries\FetchFeedQuery + { + return new \Timetabio\Frontend\Queries\FetchFeedQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFetchFeedPostsQuery(): \Timetabio\Frontend\Queries\FetchFeedPostsQuery + { + return new \Timetabio\Frontend\Queries\FetchFeedPostsQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createLookupVanityQuery(): \Timetabio\Frontend\Queries\Feed\LookupVanityQuery + { + return new \Timetabio\Frontend\Queries\Feed\LookupVanityQuery( + $this->getMasterFactory()->createDataStoreReader() + ); + } + + public function createFetchPostQuery(): \Timetabio\Frontend\Queries\Post\FetchPostQuery + { + return new \Timetabio\Frontend\Queries\Post\FetchPostQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createSearchQuery(): \Timetabio\Frontend\Queries\SearchQuery + { + return new \Timetabio\Frontend\Queries\SearchQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFetchUserFeedQuery(): \Timetabio\Frontend\Queries\FetchUserFeedQuery + { + return new \Timetabio\Frontend\Queries\FetchUserFeedQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFetchFeedInvitationsQuery(): \Timetabio\Frontend\Queries\Feed\FetchFeedInvitationsQuery + { + return new \Timetabio\Frontend\Queries\Feed\FetchFeedInvitationsQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + + public function createFetchFeedUsersQuery(): \Timetabio\Frontend\Queries\Feed\FetchFeedUsersQuery + { + return new \Timetabio\Frontend\Queries\Feed\FetchFeedUsersQuery( + $this->getMasterFactory()->createApiGateway() + ); + } + } +} diff --git a/Frontend/src/Factories/RendererFactory.php b/Frontend/src/Factories/RendererFactory.php new file mode 100644 index 0000000..022da00 --- /dev/null +++ b/Frontend/src/Factories/RendererFactory.php @@ -0,0 +1,342 @@ +getTemplate(), + $this->getMasterFactory()->createStaticPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createStaticPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\StaticPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\StaticPageRenderer( + $this->getMasterFactory()->createDomBackend(), + $this->getMasterFactory()->createGettext() + ); + } + + public function createVerifyAccountPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createVerifyAccountPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createVerifyAccountPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\Account\VerifyAccountPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\Account\VerifyAccountPageRenderer( + $this->getMasterFactory()->createDomBackend() + ); + } + + public function createHomepageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createHomepageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createHomepageContentRenderer(): \Timetabio\Frontend\Renderers\Page\HomepageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\HomepageRenderer( + $this->getMasterFactory()->createPostSnippet(), + $this->getMasterFactory()->createPaginationButtonSnippet(), + $this->getMasterFactory()->createHomepageNavigationSnippet(), + $this->getMasterFactory()->createHomepageOnboardingSnippet() + ); + } + + public function createFeedsPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createFeedsPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createFeedsPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\FeedsPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\FeedsPageRenderer( + $this->getMasterFactory()->createFeedCardSnippet(), + $this->getMasterFactory()->createPaginationButtonSnippet(), + $this->getMasterFactory()->createHomepageNavigationSnippet(), + $this->getMasterFactory()->createHomepageOnboardingSnippet(), + $this->getMasterFactory()->createFloatingButtonSnippet() + ); + } + + public function createFeedPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\FeedPageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createFeedPageContentRenderer(), + $this->getMasterFactory()->createTransformer(), + $this->getMasterFactory()->createFeedHeaderSnippet(), + $this->getMasterFactory()->createFeedButtonsSnippet(), + $this->getMasterFactory()->createFeedInvitationBannerSnippet(), + $this->getMasterFactory()->createFeedNavigationSnippet() + ); + } + + public function createFeedPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\FeedPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\FeedPageRenderer( + $this->getMasterFactory()->createPostSnippet(), + $this->getMasterFactory()->createPaginationButtonSnippet() + ); + } + + public function createPostPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createPostPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createPostPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\PostPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\PostPageRenderer( + $this->getMasterFactory()->createPostSnippet() + ); + } + + public function createCreatePostPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createCreatePostPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createCreatePostPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\CreatePostPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\CreatePostPageRenderer( + $this->getMasterFactory()->createFeedButtonsSnippet(), + $this->getMasterFactory()->createIconSnippet(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createSearchPageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\PageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createSearchPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createSearchPageContentRenderer(): \Timetabio\Frontend\Renderers\Page\SearchPageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\SearchPageRenderer( + $this->getMasterFactory()->createPostSnippet(), + $this->getMasterFactory()->createFeedCardSnippet(), + $this->getMasterFactory()->createSearchTabNavSnippet() + ); + } + + public function createFeedPeoplePageRenderer(): \Timetabio\Frontend\Renderers\Renderer + { + return new \Timetabio\Frontend\Renderers\FeedPageRenderer( + $this->getTemplate(), + $this->getMasterFactory()->createFeedPeoplePageContentRenderer(), + $this->getMasterFactory()->createTransformer(), + $this->getMasterFactory()->createFeedHeaderSnippet(), + $this->getMasterFactory()->createFeedButtonsSnippet(), + $this->getMasterFactory()->createFeedInvitationBannerSnippet(), + $this->getMasterFactory()->createFeedNavigationSnippet() + ); + } + + public function createFeedPeoplePageContentRenderer(): \Timetabio\Frontend\Renderers\Page\Feed\FeedPeoplePageRenderer + { + return new \Timetabio\Frontend\Renderers\Page\Feed\FeedPeoplePageRenderer( + $this->getMasterFactory()->createUserRolesOptionsSnippet(), + $this->getMasterFactory()->createFeedUserCardSnippet(), + $this->getMasterFactory()->createFeedInvitationCardSnippet(), + $this->getMasterFactory()->createIconButtonSnippet() + ); + } + + public function createFeedListSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedListSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedListSnippet( + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createFeedHeaderSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedHeaderSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedHeaderSnippet( + $this->getMasterFactory()->createIconSnippet() + ); + } + + public function createPostSnippet(): \Timetabio\Frontend\Renderers\Snippet\PostSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\PostSnippet( + $this->getMasterFactory()->createIconSnippet(), + $this->getMasterFactory()->createUriBuilder(), + $this->getMasterFactory()->createPostAttachmentSnippet() + ); + } + + public function createPostAttachmentSnippet(): \Timetabio\Frontend\Renderers\Snippet\PostAttachmentSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\PostAttachmentSnippet( + $this->getMasterFactory()->createIconSnippet() + ); + } + + public function createFeedButtonsSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedButtonsSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedButtonsSnippet( + $this->getMasterFactory()->createFloatingButtonSnippet(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createIconSnippet(): \Timetabio\Frontend\Renderers\Snippet\IconSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\IconSnippet; + } + + public function createFeedInvitationBannerSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedInvitationBannerSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedInvitationBannerSnippet; + } + + public function createTabNavSnippet(): \Timetabio\Frontend\Renderers\Snippet\TabNavSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\TabNavSnippet( + $this->getMasterFactory()->createIconSnippet() + ); + } + + public function createFeedCardSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedCardSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedCardSnippet( + $this->getMasterFactory()->createUriBuilder(), + $this->getMasterFactory()->createIconSnippet() + ); + } + + public function createFeedPostsFragmentRenderer(): \Timetabio\Frontend\Renderers\Fragment\FeedPostsFragmentRenderer + { + return new \Timetabio\Frontend\Renderers\Fragment\FeedPostsFragmentRenderer( + $this->getMasterFactory()->createPostSnippet() + ); + } + + public function createHomepagePostsFragmentRenderer(): \Timetabio\Frontend\Renderers\Fragment\HomepagePostsFragmentRenderer + { + return new \Timetabio\Frontend\Renderers\Fragment\HomepagePostsFragmentRenderer( + $this->getMasterFactory()->createPostSnippet() + ); + } + + public function createPaginationButtonSnippet(): \Timetabio\Frontend\Renderers\Snippet\PaginationButtonSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\PaginationButtonSnippet; + } + + public function createFeedNavigationSnippet(): \Timetabio\Frontend\Renderers\Snippet\FeedNavigationSnippet + { + return new \Timetabio\Frontend\Renderers\Snippet\FeedNavigationSnippet( + $this->getMasterFactory()->createTabNavSnippet(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createUserRolesOptionsSnippet(): Renderers\Snippet\UserRolesOptionsSnippet + { + return new Renderers\Snippet\UserRolesOptionsSnippet; + } + + public function createAjaxButtonSnippet(): Renderers\Snippet\AjaxButtonSnippet + { + return new Renderers\Snippet\AjaxButtonSnippet; + } + + public function createFeedUserCardSnippet(): Renderers\Snippet\FeedUserCardSnippet + { + return new Renderers\Snippet\FeedUserCardSnippet( + $this->getMasterFactory()->createIconButtonSnippet(), + $this->getMasterFactory()->createUserRolesOptionsSnippet(), + $this->getMasterFactory()->createAjaxButtonSnippet(), + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createIconButtonSnippet(): Renderers\Snippet\IconButtonSnippet + { + return new Renderers\Snippet\IconButtonSnippet( + $this->getMasterFactory()->createIconSnippet() + ); + } + + public function createFeedInvitationCardSnippet(): Renderers\Snippet\FeedInvitationCardSnippet + { + return new Renderers\Snippet\FeedInvitationCardSnippet( + $this->getMasterFactory()->createIconButtonSnippet(), + $this->getMasterFactory()->createAjaxButtonSnippet(), + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createSearchTabNavSnippet(): Renderers\Snippet\SearchTabNavSnippet + { + return new Renderers\Snippet\SearchTabNavSnippet( + $this->getMasterFactory()->createTabNavSnippet(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createHomepageNavigationSnippet(): Renderers\Snippet\HomepageNavigationSnippet + { + return new Renderers\Snippet\HomepageNavigationSnippet( + $this->getMasterFactory()->createTabNavSnippet() + ); + } + + public function createHomepageOnboardingSnippet(): Renderers\Snippet\HomepageOnboardingSnippet + { + return new Renderers\Snippet\HomepageOnboardingSnippet; + } + + public function createFloatingButtonSnippet(): Renderers\Snippet\FloatingButtonSnippet + { + return new Renderers\Snippet\FloatingButtonSnippet( + $this->getMasterFactory()->createIconSnippet() + ); + } + + protected function getTemplate(): \Timetabio\Framework\Dom\Document + { + return $this->getMasterFactory()->createDomBackend()->loadHtml( + 'templates://template.html' + ); + } + } +} diff --git a/Frontend/src/Factories/RouterFactory.php b/Frontend/src/Factories/RouterFactory.php new file mode 100644 index 0000000..d61c580 --- /dev/null +++ b/Frontend/src/Factories/RouterFactory.php @@ -0,0 +1,92 @@ +getMasterFactory(), + $this->getMasterFactory()->createDataStoreReader() + ); + } + + public function createNotFoundRouter(): \Timetabio\Frontend\Routers\NotFoundRouter + { + return new \Timetabio\Frontend\Routers\NotFoundRouter( + $this->getMasterFactory() + ); + } + + public function createActionRouter(): \Timetabio\Frontend\Routers\ActionRouter + { + return new \Timetabio\Frontend\Routers\ActionRouter( + $this->getMasterFactory() + ); + } + + public function createUserActionRouter(): \Timetabio\Frontend\Routers\UserActionRouter + { + return new \Timetabio\Frontend\Routers\UserActionRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createIsLoggedInQuery() + ); + } + + public function createPageRouter(): \Timetabio\Frontend\Routers\PageRouter + { + return new \Timetabio\Frontend\Routers\PageRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createIsLoggedInQuery() + ); + } + + public function createUserPageRouter(): \Timetabio\Frontend\Routers\UserPageRouter + { + return new \Timetabio\Frontend\Routers\UserPageRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createIsLoggedInQuery() + ); + } + + public function createFeedPageRouter(): \Timetabio\Frontend\Routers\FeedPageRouter + { + return new \Timetabio\Frontend\Routers\FeedPageRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createFetchFeedQuery(), + $this->getMasterFactory()->createLookupVanityQuery(), + $this->getMasterFactory()->createIsLoggedInQuery() + ); + } + + public function createPostPageRouter(): \Timetabio\Frontend\Routers\PostPageRouter + { + return new \Timetabio\Frontend\Routers\PostPageRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createFetchPostQuery() + ); + } + + public function createFragmentRouter(): \Timetabio\Frontend\Routers\FragmentRouter + { + return new \Timetabio\Frontend\Routers\FragmentRouter( + $this->getMasterFactory() + ); + } + + public function createUserFragmentRouter(): \Timetabio\Frontend\Routers\UserFragmentRouter + { + return new \Timetabio\Frontend\Routers\UserFragmentRouter( + $this->getMasterFactory(), + $this->getMasterFactory()->createIsLoggedInQuery() + ); + } + } +} diff --git a/Frontend/src/Factories/SessionFactory.php b/Frontend/src/Factories/SessionFactory.php new file mode 100644 index 0000000..bc61768 --- /dev/null +++ b/Frontend/src/Factories/SessionFactory.php @@ -0,0 +1,29 @@ +session === null) { + $this->session = new \Timetabio\Frontend\Session\Session( + $this->getMasterFactory()->createDataStoreReader() + ); + } + + return $this->session; + } + } +} diff --git a/Frontend/src/Factories/TransformationFactory.php b/Frontend/src/Factories/TransformationFactory.php new file mode 100644 index 0000000..eeaf621 --- /dev/null +++ b/Frontend/src/Factories/TransformationFactory.php @@ -0,0 +1,64 @@ +getMasterFactory()->createGettext() + ); + } + + public function createCanonicalUriTransformation(): \Timetabio\Frontend\Transformations\CanonicalUriTransformation + { + return new \Timetabio\Frontend\Transformations\CanonicalUriTransformation; + } + + public function createTrackingTransformation(): \Timetabio\Frontend\Transformations\TrackingTransformation + { + return new \Timetabio\Frontend\Transformations\TrackingTransformation( + $this->getMasterFactory()->createDomBackend() + ); + } + + public function createTransformer(): \Timetabio\Frontend\Transformations\Transformer + { + $transformations = [ + $this->getMasterFactory()->createTitleTransformation(), + $this->getMasterFactory()->createCsrfTokenTransformation(), + $this->getMasterFactory()->createUserDropdownTransformation(), + $this->getMasterFactory()->createTranslateTransformation(), + $this->getMasterFactory()->createCanonicalUriTransformation() + ]; + + if (!$this->getMasterFactory()->getConfiguration()->isDevelopmentMode()) { + $transformations[] = $this->getMasterFactory()->createTrackingTransformation(); + } + + return new \Timetabio\Frontend\Transformations\Transformer(...$transformations); + } + } +} diff --git a/Frontend/src/Gateways/ApiGateway.php b/Frontend/src/Gateways/ApiGateway.php new file mode 100644 index 0000000..1baaa7c --- /dev/null +++ b/Frontend/src/Gateways/ApiGateway.php @@ -0,0 +1,289 @@ +apiBackend = $apiBackend; + $this->session = $session; + $this->systemToken = $systemToken; + } + + public function createUser(string $email, string $username, string $password): ApiResponse + { + return $this->apiBackend->post( + '/users', + [ + 'email' => $email, + 'username' => $username, + 'password' => $password + ], + $this->systemToken + ); + } + + public function verifyUser(string $token): ApiResponse + { + return $this->apiBackend->post( + '/verify', + [ + 'token' => $token + ], + $this->systemToken + ); + } + + public function getUser(): ApiResponse + { + return $this->apiBackend->get('/user', [], $this->getAccessToken()); + } + + public function getUserFeeds(int $limit, int $page): ApiResponse + { + return $this->apiBackend->get( + '/user/feeds', + [ + 'limit' => $limit, + 'page' => $page + ], + $this->getAccessToken() + ); + } + + public function authenticate(string $user, string $password): ApiResponse + { + return $this->apiBackend->post( + '/auth', + [ + 'user' => $user, + 'password' => $password, + 'scopes' => '*', + 'auto_renew' => 'true' + ], + $this->systemToken + ); + } + + public function resendVerification(string $email): ApiResponse + { + return $this->apiBackend->post('/verify/resend', ['email' => $email], $this->systemToken); + } + + public function createFeed(string $name, string $description, bool $isPrivate): ApiResponse + { + return $this->apiBackend->post( + '/feeds', + [ + 'name' => $name, + 'description' => $description, + 'is_private' => $isPrivate ? 'true' : 'false' + ], + $this->getAccessToken() + ); + } + + public function getFeed(string $feedId): ApiResponse + { + return $this->apiBackend->get('/feeds/' . urlencode($feedId), [], $this->getAccessToken()); + } + + public function getFeedPosts(string $feedId, int $limit, int $page): ApiResponse + { + return $this->apiBackend->get( + '/feeds/' . urlencode($feedId) . '/posts', + ['limit' => $limit, 'page' => $page], + $this->getAccessToken() + ); + } + + public function createNote(string $feedId, string $title, string $body, array $attachments): ApiResponse + { + return $this->apiBackend->post( + '/feeds/' . urlencode($feedId) . '/posts', + [ + 'type' => 'note', + 'title' => $title, + 'body' => $body, + 'attachments' => $attachments + ], + $this->getAccessToken() + ); + } + + public function deletePost(string $postId): ApiResponse + { + return $this->apiBackend->delete( + '/posts/' . urlencode($postId), + [], + $this->getAccessToken() + ); + } + + public function followFeed(string $feedId): ApiResponse + { + return $this->apiBackend->post( + '/feeds/' . urlencode($feedId) . '/follow', + [], + $this->getAccessToken() + ); + } + + public function unfollowFeed(string $feedId): ApiResponse + { + return $this->apiBackend->post( + '/feeds/' . urlencode($feedId) . '/unfollow', + [], + $this->getAccessToken() + ); + } + + public function createUpload(string $filename, string $mimeType): ApiResponse + { + return $this->apiBackend->post( + '/upload', + [ + 'filename' => $filename, + 'mime_type' => $mimeType + ], + $this->getAccessToken() + ); + } + + public function getPost(string $postId): ApiResponse + { + return $this->apiBackend->get('/posts/' . urlencode($postId), [], $this->getAccessToken()); + } + + public function revokeToken(string $token): ApiResponse + { + return $this->apiBackend->post('/revoke', [], new BearerToken($token)); + } + + public function createBetaRequest(string $email): ApiResponse + { + return $this->apiBackend->post( + '/beta_requests', + [ + 'email' => $email + ], + $this->systemToken + ); + } + + public function search(string $query, string $type): ApiResponse + { + return $this->apiBackend->get( + '/search', + [ + 'query' => $query, + 'type' => $type + ], + $this->getAccessToken() + ); + } + + public function getUserFeed(int $limit = 20, int $page = 1): ApiResponse + { + return $this->apiBackend->get( + '/user/feed', + [ + 'limit' => $limit, + 'page' => $page + ], + $this->getAccessToken() + ); + } + + public function getFeedInvitations(string $feedId): ApiResponse + { + return $this->apiBackend->get( + '/feeds/' . urlencode($feedId) . '/invitations', + [], + $this->getAccessToken() + ); + } + + public function getFeedUsers(string $feedId): ApiResponse + { + return $this->apiBackend->get( + '/feeds/' . urlencode($feedId) . '/users', + [], + $this->getAccessToken() + ); + } + + public function deleteFeedUser(string $feedId, string $userId): ApiResponse + { + return $this->apiBackend->delete( + '/feeds/' . urlencode($feedId) . '/users/' . urlencode($userId), + [], + $this->getAccessToken() + ); + } + + public function inviteFeedUser(string $feedId, string $username, string $role): ApiResponse + { + return $this->apiBackend->post( + '/feeds/' . urlencode($feedId) . '/invitations', + [ + 'username' => $username, + 'role' => $role + ], + $this->getAccessToken() + ); + } + + public function deleteFeedInvitation(string $feedId, string $userId) + { + return $this->apiBackend->delete( + '/feeds/' . urlencode($feedId) . '/invitations/' . urlencode($userId), + [], + $this->getAccessToken() + ); + } + + public function updateFeedUser(string $feedId, string $userId, string $role) + { + return $this->apiBackend->patch( + '/feeds/' . urlencode($feedId) . '/users/' . urlencode($userId), + [ + 'role' => $role + ], + $this->getAccessToken() + ); + } + + protected function getAccessToken() + { + if (!$this->session->hasAccessToken()) { + return null; + } + + return new BearerToken($this->session->getAccessToken()); + } + } +} diff --git a/Frontend/src/Handlers/CommandHandler.php b/Frontend/src/Handlers/CommandHandler.php new file mode 100644 index 0000000..92102d1 --- /dev/null +++ b/Frontend/src/Handlers/CommandHandler.php @@ -0,0 +1,17 @@ +verifyCommand = $verifyCommand; + } + + public function execute(AbstractModel $model) + { + /** @var VerifyModel $model */ + + $model->setTitle($this->getTranslator()->translate('Verify')); + + if ($model->hasStatusCode() && $model->getStatusCode() instanceof \Timetabio\Framework\Http\StatusCodes\NotFound) { + return; + } + + try { + $this->verifyCommand->execute($model->getToken()); + } catch (ApiException $exception) { + $error = $exception->getMessage(); + + if ($error === 'invalid_token') { + $model->setStatusCode(new \Timetabio\Framework\Http\StatusCodes\NotFound); + } else { + throw new \RuntimeException('unexpected api response'); + } + } + } + } +} diff --git a/Frontend/src/Handlers/Get/Account/Verify/RequestHandler.php b/Frontend/src/Handlers/Get/Account/Verify/RequestHandler.php new file mode 100644 index 0000000..4bc12e7 --- /dev/null +++ b/Frontend/src/Handlers/Get/Account/Verify/RequestHandler.php @@ -0,0 +1,26 @@ +hasQueryParam('token')) { + $model->setStatusCode(new \Timetabio\Framework\Http\StatusCodes\NotFound); + return; + } + + $model->setToken($request->getQueryParam('token')); + } + } +} diff --git a/Frontend/src/Handlers/Get/CreatePostPage/QueryHandler.php b/Frontend/src/Handlers/Get/CreatePostPage/QueryHandler.php new file mode 100644 index 0000000..4d0c487 --- /dev/null +++ b/Frontend/src/Handlers/Get/CreatePostPage/QueryHandler.php @@ -0,0 +1,24 @@ +setTitle($this->getTranslator()->translate('Create New Post')); + } + } +} diff --git a/Frontend/src/Handlers/Get/FeedPage/QueryHandler.php b/Frontend/src/Handlers/Get/FeedPage/QueryHandler.php new file mode 100644 index 0000000..19d5944 --- /dev/null +++ b/Frontend/src/Handlers/Get/FeedPage/QueryHandler.php @@ -0,0 +1,45 @@ +fetchFeedPostsQuery = $fetchFeedPostsQuery; + $this->uriBuilder = $uriBuilder; + } + + public function execute(AbstractModel $model) + { + /** @var FeedPostsPageModel $model */ + + $feed = $model->getFeed(); + $feedId = $feed->getId(); + + $posts = $this->fetchFeedPostsQuery->execute($feedId); + + $model->setTitle($feed->getName()); + $model->setFeedPosts($posts); + $model->setCanonicalUri($this->uriBuilder->buildFeedPageUri($feedId)); + } + } +} diff --git a/Frontend/src/Handlers/Get/FeedPeoplePage/CommandHandler.php b/Frontend/src/Handlers/Get/FeedPeoplePage/CommandHandler.php new file mode 100644 index 0000000..1da7789 --- /dev/null +++ b/Frontend/src/Handlers/Get/FeedPeoplePage/CommandHandler.php @@ -0,0 +1,17 @@ +fetchFeedUsersQuery = $fetchFeedUsersQuery; + $this->fetchFeedInvitationsQuery = $fetchFeedInvitationsQuery; + } + + public function execute(AbstractModel $model) + { + /** @var \Timetabio\Frontend\Models\Page\FeedPeoplePageModel $model */ + + $feed = $model->getFeed(); + $feedId = $feed->getId(); + + if ($feed->hasUsersManageAccess()) { + $model->setFeedInvitations( + $this->fetchFeedInvitationsQuery->execute($feedId) + ); + } + + $users = $this->fetchFeedUsersQuery->execute($feedId); + + if ($users !== null) { + $model->setFeedUsers($users); + } + + $model->setTitle($feed->getName()); + } + } +} diff --git a/Frontend/src/Handlers/Get/FeedPeoplePage/RequestHandler.php b/Frontend/src/Handlers/Get/FeedPeoplePage/RequestHandler.php new file mode 100644 index 0000000..23d2dcb --- /dev/null +++ b/Frontend/src/Handlers/Get/FeedPeoplePage/RequestHandler.php @@ -0,0 +1,18 @@ +fetchFeedQuery = $fetchFeedQuery; + $this->fetchFeedPostsQuery = $fetchFeedPostsQuery; + } + + public function execute(AbstractModel $model) + { + /** @var FeedPostsFragmentModel $model */ + + $model->setFeed($this->fetchFeedQuery->execute( + $model->getFeedId() + )); + + $model->setPosts($this->fetchFeedPostsQuery->execute( + $model->getFeedId(), + $model->getLimit(), + $model->getPage() + )); + } + } +} diff --git a/Frontend/src/Handlers/Get/FeedPostsFragment/RequestHandler.php b/Frontend/src/Handlers/Get/FeedPostsFragment/RequestHandler.php new file mode 100644 index 0000000..4bcfb92 --- /dev/null +++ b/Frontend/src/Handlers/Get/FeedPostsFragment/RequestHandler.php @@ -0,0 +1,38 @@ +getUri()->getExplodedPath(); + + $model->setFeedId($parts[2]); + + try { + $limit = (int) $request->getQueryParam('limit'); + } catch (\Throwable $exception) { + $limit = 20; + } + + try { + $page = (int) $request->getQueryParam('page'); + } catch (\Throwable $exception) { + $page = 1; + } + + $model->setLimit($limit); + $model->setPage($page); + } + } +} diff --git a/Frontend/src/Handlers/Get/FeedsPage/QueryHandler.php b/Frontend/src/Handlers/Get/FeedsPage/QueryHandler.php new file mode 100644 index 0000000..ed45366 --- /dev/null +++ b/Frontend/src/Handlers/Get/FeedsPage/QueryHandler.php @@ -0,0 +1,36 @@ +fetchUserFeedsQuery = $fetchUserFeedsQuery; + } + + public function execute(AbstractModel $model) + { + /** @var FeedsPageModel $model */ + + $model->setTitle($this->getTranslator()->translate('Feeds')); + $model->setFeeds($this->fetchUserFeedsQuery->execute()); + } + } +} diff --git a/Frontend/src/Handlers/Get/Fragment/TransformationHandler.php b/Frontend/src/Handlers/Get/Fragment/TransformationHandler.php new file mode 100644 index 0000000..4bc017d --- /dev/null +++ b/Frontend/src/Handlers/Get/Fragment/TransformationHandler.php @@ -0,0 +1,34 @@ +fragmentRenderer = $fragmentRenderer; + } + + public function execute(AbstractModel $model): string + { + /** @var FragmentModel $model */ + + return $this->fragmentRenderer->render($model); + } + } +} diff --git a/Frontend/src/Handlers/Get/Homepage/QueryHandler.php b/Frontend/src/Handlers/Get/Homepage/QueryHandler.php new file mode 100644 index 0000000..37bf47b --- /dev/null +++ b/Frontend/src/Handlers/Get/Homepage/QueryHandler.php @@ -0,0 +1,36 @@ +fetchUserFeedQuery = $fetchUserFeedQuery; + } + + public function execute(AbstractModel $model) + { + /** @var HomepageModel $model */ + + $model->setTitle($this->getTranslator()->translate('Home')); + $model->setPosts($this->fetchUserFeedQuery->execute()); + } + } +} diff --git a/Frontend/src/Handlers/Get/HomepagePostsFragment/QueryHandler.php b/Frontend/src/Handlers/Get/HomepagePostsFragment/QueryHandler.php new file mode 100644 index 0000000..5150a72 --- /dev/null +++ b/Frontend/src/Handlers/Get/HomepagePostsFragment/QueryHandler.php @@ -0,0 +1,34 @@ +fetchUserFeedQuery = $fetchUserFeedQuery; + } + + public function execute(AbstractModel $model) + { + /** @var HomepagePostsFragmentModel $model */ + + $model->setPosts($this->fetchUserFeedQuery->execute( + $model->getLimit(), + $model->getPage() + )); + } + } +} diff --git a/Frontend/src/Handlers/Get/HomepagePostsFragment/RequestHandler.php b/Frontend/src/Handlers/Get/HomepagePostsFragment/RequestHandler.php new file mode 100644 index 0000000..ac84e31 --- /dev/null +++ b/Frontend/src/Handlers/Get/HomepagePostsFragment/RequestHandler.php @@ -0,0 +1,34 @@ +getQueryParam('limit'); + } catch (\Throwable $exception) { + $limit = 20; + } + + try { + $page = (int) $request->getQueryParam('page'); + } catch (\Throwable $exception) { + $page = 1; + } + + $model->setLimit($limit); + $model->setPage($page); + } + } +} diff --git a/Frontend/src/Handlers/Get/Page/PreHandler.php b/Frontend/src/Handlers/Get/Page/PreHandler.php new file mode 100644 index 0000000..9336b40 --- /dev/null +++ b/Frontend/src/Handlers/Get/Page/PreHandler.php @@ -0,0 +1,43 @@ +session = $session; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var FrontendModel $model */ + + $session = $this->session; + + $model->setCrfsToken($session->getCrfsToken()); + + if ($session->hasUser()) { + $model->setUser($session->getUser()); + } + + if ($request->isDnt() && $model instanceof PageModel) { + $model->disableTracking(); + } + } + } +} diff --git a/Frontend/src/Handlers/Get/Page/TransformationHandler.php b/Frontend/src/Handlers/Get/Page/TransformationHandler.php new file mode 100644 index 0000000..ad1fc8f --- /dev/null +++ b/Frontend/src/Handlers/Get/Page/TransformationHandler.php @@ -0,0 +1,31 @@ +renderer = $renderer; + } + + public function execute(AbstractModel $model): string + { + /** @var PageModel $model */ + + return $this->renderer->render($model); + } + } +} diff --git a/Frontend/src/Handlers/Get/PostPage/QueryHandler.php b/Frontend/src/Handlers/Get/PostPage/QueryHandler.php new file mode 100644 index 0000000..4ece486 --- /dev/null +++ b/Frontend/src/Handlers/Get/PostPage/QueryHandler.php @@ -0,0 +1,24 @@ +getPost(); + + if (isset($post['title'])) { + $model->setTitle($post['title']); + } + } + } +} diff --git a/Frontend/src/Handlers/Get/SearchPage/QueryHandler.php b/Frontend/src/Handlers/Get/SearchPage/QueryHandler.php new file mode 100644 index 0000000..193a7be --- /dev/null +++ b/Frontend/src/Handlers/Get/SearchPage/QueryHandler.php @@ -0,0 +1,55 @@ +searchQuery = $searchQuery; + $this->searchTabLocator = $searchTabLocator; + } + + public function execute(AbstractModel $model) + { + /** @var SearchPageModel $model */ + + $searchType = $model->getSearchType(); + + $model->setTitle($this->getTranslator()->translate('Search')); + + $model->setSearchResults( + $this->searchQuery->execute( + $model->getSearchQuery(), + $searchType + ) + ); + + $model->setActiveTab( + $this->searchTabLocator->locate($searchType) + ); + } + } +} diff --git a/Frontend/src/Handlers/Get/SearchPage/RequestHandler.php b/Frontend/src/Handlers/Get/SearchPage/RequestHandler.php new file mode 100644 index 0000000..265c231 --- /dev/null +++ b/Frontend/src/Handlers/Get/SearchPage/RequestHandler.php @@ -0,0 +1,23 @@ +hasQueryParam('q')) { + $model->setSearchQuery($request->getQueryParam('q')); + } + } + } +} diff --git a/Frontend/src/Handlers/Get/StaticPage/QueryHandler.php b/Frontend/src/Handlers/Get/StaticPage/QueryHandler.php new file mode 100644 index 0000000..de01b9d --- /dev/null +++ b/Frontend/src/Handlers/Get/StaticPage/QueryHandler.php @@ -0,0 +1,47 @@ +fetchStaticPageQuery = $fetchStaticPageQuery; + } + + public function execute(AbstractModel $model) + { + /** @var StaticPageModel $model */ + + $staticPage = $this->fetchStaticPageQuery->execute( + $model->getName(), + $model->getLanguage() + ); + + if ($staticPage->hasCode()) { + $model->setStatusCode($staticPage->getCode()); + } + + $title = $this->getTranslator()->translate($staticPage->getTitle()); + + $model->setTitle($title); + $model->setStaticPage($staticPage); + } + } +} diff --git a/Frontend/src/Handlers/Post/CreateBetaRequest/CommandHandler.php b/Frontend/src/Handlers/Post/CreateBetaRequest/CommandHandler.php new file mode 100644 index 0000000..50c8729 --- /dev/null +++ b/Frontend/src/Handlers/Post/CreateBetaRequest/CommandHandler.php @@ -0,0 +1,45 @@ +createBetaRequestCommand = $createBetaRequestCommand; + } + + public function execute(AbstractModel $model) + { + /** @var CreateBetaRequestModel $model */ + + try { + $this->createBetaRequestCommand->execute($model->getEmail()); + } catch (ApiException $exception) { + throw new BadRequest($this->getTranslator()->translate($exception)); + } + + $model->setData([ + 'redirect' => '/beta/thanks' + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/CreateBetaRequest/RequestHandler.php b/Frontend/src/Handlers/Post/CreateBetaRequest/RequestHandler.php new file mode 100644 index 0000000..631f1ce --- /dev/null +++ b/Frontend/src/Handlers/Post/CreateBetaRequest/RequestHandler.php @@ -0,0 +1,28 @@ +setEmail($request->getParam('email')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/CreateNote/CommandHandler.php b/Frontend/src/Handlers/Post/CreateNote/CommandHandler.php new file mode 100644 index 0000000..ea4fb45 --- /dev/null +++ b/Frontend/src/Handlers/Post/CreateNote/CommandHandler.php @@ -0,0 +1,59 @@ +createNoteCommand = $createNoteCommand; + $this->uriBuilder = $uriBuilder; + } + + public function execute(AbstractModel $model) + { + /** @var CreateNoteModel $model */ + + $model->setData($this->process($model)); + } + + private function process(CreateNoteModel $model): array + { + try { + $post = $this->createNoteCommand->execute( + $model->getFeedId(), + $model->getPostTitle(), + $model->getPostBody(), + $model->getAttachments() + ); + + return [ + 'redirect' => $this->uriBuilder->buildPostPageUri($post['id']) + ]; + } catch (ApiException $exception) { + return [ + 'error' => $exception->getMessage() + ]; + } + } + } +} diff --git a/Frontend/src/Handlers/Post/CreateNote/RequestHandler.php b/Frontend/src/Handlers/Post/CreateNote/RequestHandler.php new file mode 100644 index 0000000..a6c10fe --- /dev/null +++ b/Frontend/src/Handlers/Post/CreateNote/RequestHandler.php @@ -0,0 +1,44 @@ +setFeedId($request->getParam('feed_id')); + $model->setPostTitle($request->getParam('title')); + $model->setPostBody($request->getParam('body')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + + if (!$request->hasParam('attachments')) { + return; + } + + $attachments = $request->getParam('attachments'); + + if (!is_array($attachments)) { + return; + } + + foreach ($attachments as $attachment) { + $model->addAttachment($attachment); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/DeleteFeedInvitation/CommandHandler.php b/Frontend/src/Handlers/Post/DeleteFeedInvitation/CommandHandler.php new file mode 100644 index 0000000..ed2763c --- /dev/null +++ b/Frontend/src/Handlers/Post/DeleteFeedInvitation/CommandHandler.php @@ -0,0 +1,38 @@ +deleteFeedInvitationCommand = $deleteFeedInvitationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var DeleteFeedUserModel $model */ + + $this->deleteFeedInvitationCommand->execute( + $model->getFeedId(), + $model->getUserId() + ); + + $model->setData([ + 'reload' => true + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/DeleteFeedInvitation/RequestHandler.php b/Frontend/src/Handlers/Post/DeleteFeedInvitation/RequestHandler.php new file mode 100644 index 0000000..0e11207 --- /dev/null +++ b/Frontend/src/Handlers/Post/DeleteFeedInvitation/RequestHandler.php @@ -0,0 +1,29 @@ +setFeedId($request->getParam('feed_id')); + $model->setUserId($request->getParam('user_id')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/DeleteFeedUser/CommandHandler.php b/Frontend/src/Handlers/Post/DeleteFeedUser/CommandHandler.php new file mode 100644 index 0000000..3cc1f88 --- /dev/null +++ b/Frontend/src/Handlers/Post/DeleteFeedUser/CommandHandler.php @@ -0,0 +1,39 @@ +deleteFeedUserCommand = $deleteFeedUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var DeleteFeedUserModel $model */ + + try { + $this->deleteFeedUserCommand->execute($model->getFeedId(), $model->getUserId()); + } catch (ApiException $exception) { + } + + $model->setData([ + 'reload' => true + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/DeleteFeedUser/RequestHandler.php b/Frontend/src/Handlers/Post/DeleteFeedUser/RequestHandler.php new file mode 100644 index 0000000..7a7ff84 --- /dev/null +++ b/Frontend/src/Handlers/Post/DeleteFeedUser/RequestHandler.php @@ -0,0 +1,29 @@ +setFeedId($request->getParam('feed_id')); + $model->setUserId($request->getParam('user_id')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/DeletePost/CommandHandler.php b/Frontend/src/Handlers/Post/DeletePost/CommandHandler.php new file mode 100644 index 0000000..c58e764 --- /dev/null +++ b/Frontend/src/Handlers/Post/DeletePost/CommandHandler.php @@ -0,0 +1,42 @@ +deletePostCommand = $deletePostCommand; + $this->uriBuilder = $uriBuilder; + } + + public function execute(AbstractModel $model) + { + /** @var DeletePostModel $model */ + + $this->deletePostCommand->execute($model->getPostId()); + + $model->setData([ + 'redirect' => $this->uriBuilder->buildFeedPageUri($model->getFeedId()) + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/DeletePost/RequestHandler.php b/Frontend/src/Handlers/Post/DeletePost/RequestHandler.php new file mode 100644 index 0000000..66bb7e2 --- /dev/null +++ b/Frontend/src/Handlers/Post/DeletePost/RequestHandler.php @@ -0,0 +1,29 @@ +setPostId($request->getParam('post_id')); + $model->setFeedId($request->getParam('feed_id')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/Follow/CommandHandler.php b/Frontend/src/Handlers/Post/Follow/CommandHandler.php new file mode 100644 index 0000000..9d99fa4 --- /dev/null +++ b/Frontend/src/Handlers/Post/Follow/CommandHandler.php @@ -0,0 +1,40 @@ +followFeedCommand = $followFeedCommand; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + $result = $this->followFeedCommand->execute($model->getFeedId()); + + if ($result === null) { + throw new BadRequest('feed does not exist'); + } + + $model->setData([ + 'reload' => true + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/Follow/RequestHandler.php b/Frontend/src/Handlers/Post/Follow/RequestHandler.php new file mode 100644 index 0000000..ee518e3 --- /dev/null +++ b/Frontend/src/Handlers/Post/Follow/RequestHandler.php @@ -0,0 +1,28 @@ +setFeedId($request->getParam('feed_id')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/InviteFeedUser/CommandHandler.php b/Frontend/src/Handlers/Post/InviteFeedUser/CommandHandler.php new file mode 100644 index 0000000..8f1d679 --- /dev/null +++ b/Frontend/src/Handlers/Post/InviteFeedUser/CommandHandler.php @@ -0,0 +1,48 @@ +inviteFeedUserCommand = $inviteFeedUserCommand; + } + + public function execute(AbstractModel $model) + { + /** @var InviteFeedUserModel $model */ + + try { + $this->inviteFeedUserCommand->execute($model->getFeedId(), $model->getUsername(), $model->getRole()); + } catch (ApiException $exception) { + $model->setData([ + 'error' => $this->getTranslator()->translate($exception->getMessage()) + ]); + + return; + } + + $model->setData([ + 'reload' => true + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/InviteFeedUser/RequestHandler.php b/Frontend/src/Handlers/Post/InviteFeedUser/RequestHandler.php new file mode 100644 index 0000000..0b57619 --- /dev/null +++ b/Frontend/src/Handlers/Post/InviteFeedUser/RequestHandler.php @@ -0,0 +1,30 @@ +setFeedId($request->getParam('feed_id')); + $model->setUsername($request->getParam('username')); + $model->setRole($request->getParam('role')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/Login/CommandHandler.php b/Frontend/src/Handlers/Post/Login/CommandHandler.php new file mode 100644 index 0000000..ff0e637 --- /dev/null +++ b/Frontend/src/Handlers/Post/Login/CommandHandler.php @@ -0,0 +1,42 @@ +loginCommand = $loginCommand; + } + + public function execute(AbstractModel $model) + { + /** @var LoginModel $model */ + + $data = ['redirect' => '/']; + + try { + $this->loginCommand->execute($model->getLoginUser(), $model->getPassword()); + } catch (ApiException $exception) { + $data = [ + 'error' => $exception->getMessage() + ]; + } + + $model->setData($data); + } + } +} diff --git a/Frontend/src/Handlers/Post/Login/RequestHandler.php b/Frontend/src/Handlers/Post/Login/RequestHandler.php new file mode 100644 index 0000000..f024cde --- /dev/null +++ b/Frontend/src/Handlers/Post/Login/RequestHandler.php @@ -0,0 +1,29 @@ +setLoginUser($request->getParam('user')); + $model->setPassword($request->getParam('password')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/Logout/CommandHandler.php b/Frontend/src/Handlers/Post/Logout/CommandHandler.php new file mode 100644 index 0000000..c9e1c66 --- /dev/null +++ b/Frontend/src/Handlers/Post/Logout/CommandHandler.php @@ -0,0 +1,37 @@ +logoutCommand = $logoutCommand; + } + + public function execute(AbstractModel $model) + { + /** @var ActionModel $model */ + + $this->logoutCommand->execute(); + + // TODO: accept redirect + + $model->setData([ + 'redirect' => '/' + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/NewFeed/CommandHandler.php b/Frontend/src/Handlers/Post/NewFeed/CommandHandler.php new file mode 100644 index 0000000..0d7f04e --- /dev/null +++ b/Frontend/src/Handlers/Post/NewFeed/CommandHandler.php @@ -0,0 +1,58 @@ +createFeedCommand = $createFeedCommand; + $this->uriBuilder = $uriBuilder; + } + + public function execute(AbstractModel $model) + { + /** @var NewFeedModel $model */ + + $model->setData($this->process($model)); + } + + private function process(NewFeedModel $model): array + { + try { + $feed = $this->createFeedCommand->execute( + $model->getFeedName(), + $model->getFeedDescription(), + $model->getFeedIsPrivate() + ); + + return [ + 'redirect' => $this->uriBuilder->buildFeedPageUri($feed['id']) + ]; + } catch (ApiException $exception) { + return [ + 'error' => $exception->getMessage() + ]; + } + } + } +} diff --git a/Frontend/src/Handlers/Post/NewFeed/RequestHandler.php b/Frontend/src/Handlers/Post/NewFeed/RequestHandler.php new file mode 100644 index 0000000..7155348 --- /dev/null +++ b/Frontend/src/Handlers/Post/NewFeed/RequestHandler.php @@ -0,0 +1,32 @@ +getParam('visibility'); + + $model->setFeedIsPrivate($visibility === 'private'); + $model->setFeedName($request->getParam('name')); + $model->setFeedDescription($request->getParam('description')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/PreHandler.php b/Frontend/src/Handlers/Post/PreHandler.php new file mode 100644 index 0000000..5d970b1 --- /dev/null +++ b/Frontend/src/Handlers/Post/PreHandler.php @@ -0,0 +1,46 @@ +session = $session; + } + + public function execute(RequestInterface $request, AbstractModel $model) + { + /** @var PostRequest $request */ + + if (!$this->checkCrfsToken($request)) { + throw new BadRequest('invalid crfs token'); + } + } + + private function checkCrfsToken(PostRequest $request): bool + { + if (!$request->hasParam('token')) { + return false; + } + + $token = $request->getParam('token'); + + return $token === (string) $this->session->getCrfsToken(); + } + } +} diff --git a/Frontend/src/Handlers/Post/Register/CommandHandler.php b/Frontend/src/Handlers/Post/Register/CommandHandler.php new file mode 100644 index 0000000..80ffac4 --- /dev/null +++ b/Frontend/src/Handlers/Post/Register/CommandHandler.php @@ -0,0 +1,51 @@ +registerCommand = $registerCommand; + } + + public function execute(AbstractModel $model) + { + /** @var RegisterModel $model */ + + $model->setData($this->doExecute($model)); + } + + private function doExecute(RegisterModel $model): array + { + try { + $this->registerCommand->execute($model->getEmail(), $model->getUsername(), $model->getPassword()); + } catch (ApiException $exception) { + return [ + 'error' => $this->getTranslator()->translate($exception->getMessage()) + ]; + } + + return [ + 'redirect' => '/register/confirmation' + ]; + } + } +} diff --git a/Frontend/src/Handlers/Post/Register/RequestHandler.php b/Frontend/src/Handlers/Post/Register/RequestHandler.php new file mode 100644 index 0000000..22521a8 --- /dev/null +++ b/Frontend/src/Handlers/Post/Register/RequestHandler.php @@ -0,0 +1,30 @@ +setEmail($request->getParam('email')); + $model->setUsername($request->getParam('username')); + $model->setPassword($request->getParam('password')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/ResendVerification/CommandHandler.php b/Frontend/src/Handlers/Post/ResendVerification/CommandHandler.php new file mode 100644 index 0000000..2aa61c9 --- /dev/null +++ b/Frontend/src/Handlers/Post/ResendVerification/CommandHandler.php @@ -0,0 +1,42 @@ +resendVerificationCommand = $resendVerificationCommand; + } + + public function execute(AbstractModel $model) + { + /** @var ResendVerificationModel $model */ + + $data = ['redirect' => '/register/confirmation']; + + try { + $this->resendVerificationCommand->execute($model->getEmail()); + } catch (ApiException $exception) { + $data = [ + 'error' => $exception->getMessage() + ]; + } + + $model->setData($data); + } + } +} diff --git a/Frontend/src/Handlers/Post/ResendVerification/RequestHandler.php b/Frontend/src/Handlers/Post/ResendVerification/RequestHandler.php new file mode 100644 index 0000000..0d95b74 --- /dev/null +++ b/Frontend/src/Handlers/Post/ResendVerification/RequestHandler.php @@ -0,0 +1,28 @@ +setEmail($request->getParam('email')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/TransformationHandler.php b/Frontend/src/Handlers/Post/TransformationHandler.php new file mode 100644 index 0000000..3bef5ad --- /dev/null +++ b/Frontend/src/Handlers/Post/TransformationHandler.php @@ -0,0 +1,30 @@ +getData(); + + if (isset($data['error'])) { + $data['error'] = $this->getTranslator()->translate($data['error']); + } + + return json_encode($data, JSON_PRETTY_PRINT); + } + } +} diff --git a/Frontend/src/Handlers/Post/Unfollow/CommandHandler.php b/Frontend/src/Handlers/Post/Unfollow/CommandHandler.php new file mode 100644 index 0000000..beb2c56 --- /dev/null +++ b/Frontend/src/Handlers/Post/Unfollow/CommandHandler.php @@ -0,0 +1,40 @@ +unfollowFeedCommand = $unfollowFeedCommand; + } + + public function execute(AbstractModel $model) + { + /** @var FollowModel $model */ + + $result = $this->unfollowFeedCommand->execute($model->getFeedId()); + + if ($result === null) { + throw new BadRequest('feed does not exist'); + } + + $model->setData([ + 'reload' => true + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/UpdateFeedUserRole/CommandHandler.php b/Frontend/src/Handlers/Post/UpdateFeedUserRole/CommandHandler.php new file mode 100644 index 0000000..1605dfd --- /dev/null +++ b/Frontend/src/Handlers/Post/UpdateFeedUserRole/CommandHandler.php @@ -0,0 +1,41 @@ +updateFeedUserRoleCommand = $updateFeedUserRoleCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UpdateFeedUserRoleModel $model */ + + $this->updateFeedUserRoleCommand->execute( + $model->getFeedId(), + $model->getUserId(), + $model->getRole() + ); + + $model->setData([ + 'toast' => [ + 'message' => 'The user\'s role has been updated' + ] + ]); + } + } +} diff --git a/Frontend/src/Handlers/Post/UpdateFeedUserRole/RequestHandler.php b/Frontend/src/Handlers/Post/UpdateFeedUserRole/RequestHandler.php new file mode 100644 index 0000000..a59394b --- /dev/null +++ b/Frontend/src/Handlers/Post/UpdateFeedUserRole/RequestHandler.php @@ -0,0 +1,30 @@ +setFeedId($request->getParam('feed_id')); + $model->setUserId($request->getParam('user_id')); + $model->setRole($request->getParam('role')); + } catch (\Exception $exception) { + throw new BadRequest('missing fields'); + } + } + } +} diff --git a/Frontend/src/Handlers/Post/Upload/CommandHandler.php b/Frontend/src/Handlers/Post/Upload/CommandHandler.php new file mode 100644 index 0000000..2cfebe2 --- /dev/null +++ b/Frontend/src/Handlers/Post/Upload/CommandHandler.php @@ -0,0 +1,34 @@ +createUploadCommand = $createUploadCommand; + } + + public function execute(AbstractModel $model) + { + /** @var UploadModel $model */ + + $model->setData($this->createUploadCommand->execute( + $model->getFilename(), + $model->getMimeType() + )); + } + } +} diff --git a/Frontend/src/Handlers/Post/Upload/RequestHandler.php b/Frontend/src/Handlers/Post/Upload/RequestHandler.php new file mode 100644 index 0000000..e5ef313 --- /dev/null +++ b/Frontend/src/Handlers/Post/Upload/RequestHandler.php @@ -0,0 +1,29 @@ +setFilename($request->getParam('filename')); + $model->setMimeType($request->getParam('mime_type')); + } catch (\Exception $exception) { + throw new BadRequest('missing parameters'); + } + } + } +} diff --git a/Frontend/src/Handlers/PostHandler.php b/Frontend/src/Handlers/PostHandler.php new file mode 100644 index 0000000..c9ae1f9 --- /dev/null +++ b/Frontend/src/Handlers/PostHandler.php @@ -0,0 +1,35 @@ +session = $session; + $this->writeSessionCommand = $writeSessionCommand; + } + + public function execute(AbstractModel $model) + { + $this->writeSessionCommand->execute($this->session); + } + } +} diff --git a/Frontend/src/Handlers/PreHandler.php b/Frontend/src/Handlers/PreHandler.php new file mode 100644 index 0000000..86380d5 --- /dev/null +++ b/Frontend/src/Handlers/PreHandler.php @@ -0,0 +1,18 @@ +session = $session; + } + + public function execute(ResponseInterface $response, AbstractModel $model) + { + /** @var \Timetabio\Frontend\Models\FrontendModel $model */ + + $response->setCookie($this->session->getCookie()); + + $this->setStatusCode($response, $model); + } + + private function setStatusCode(ResponseInterface $response, \Timetabio\Frontend\Models\FrontendModel $model) + { + if (!$model->hasStatusCode()) { + return; + } + + $response->setStatusCode($model->getStatusCode()); + } + } +} diff --git a/Frontend/src/Locators/SearchTabLocator.php b/Frontend/src/Locators/SearchTabLocator.php new file mode 100644 index 0000000..4c6e3e5 --- /dev/null +++ b/Frontend/src/Locators/SearchTabLocator.php @@ -0,0 +1,28 @@ +feedName; + } + + public function setFeedName(string $feedName) + { + $this->feedName = $feedName; + } + + public function getFeedDescription(): string + { + return $this->feedDescription; + } + + public function setFeedDescription(string $feedDescription) + { + $this->feedDescription = $feedDescription; + } + + public function getFeedIsPrivate(): bool + { + return $this->feedIsPrivate; + } + + public function setFeedIsPrivate(bool $feedIsPrivate) + { + $this->feedIsPrivate = $feedIsPrivate; + } + } +} diff --git a/Frontend/src/Models/Account/VerifyModel.php b/Frontend/src/Models/Account/VerifyModel.php new file mode 100644 index 0000000..3ffa9b0 --- /dev/null +++ b/Frontend/src/Models/Account/VerifyModel.php @@ -0,0 +1,26 @@ +token; + } + + public function setToken(string $token) + { + $this->token = $token; + } + } +} diff --git a/Frontend/src/Models/Action/CreateBetaRequestModel.php b/Frontend/src/Models/Action/CreateBetaRequestModel.php new file mode 100644 index 0000000..c746888 --- /dev/null +++ b/Frontend/src/Models/Action/CreateBetaRequestModel.php @@ -0,0 +1,26 @@ +email; + } + + public function setEmail(string $email) + { + $this->email = $email; + } + } +} diff --git a/Frontend/src/Models/Action/CreateNoteModel.php b/Frontend/src/Models/Action/CreateNoteModel.php new file mode 100644 index 0000000..8e1f031 --- /dev/null +++ b/Frontend/src/Models/Action/CreateNoteModel.php @@ -0,0 +1,71 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getPostTitle(): string + { + return $this->postTitle; + } + + public function setPostTitle(string $postTitle) + { + $this->postTitle = $postTitle; + } + + public function getPostBody(): string + { + return $this->postBody; + } + + public function setPostBody(string $postBody) + { + $this->postBody = $postBody; + } + + public function getAttachments(): array + { + return $this->attachments; + } + + public function addAttachment(string $attachment) + { + $this->attachments[] = $attachment; + } + } +} diff --git a/Frontend/src/Models/Action/DeleteFeedUserModel.php b/Frontend/src/Models/Action/DeleteFeedUserModel.php new file mode 100644 index 0000000..8a93992 --- /dev/null +++ b/Frontend/src/Models/Action/DeleteFeedUserModel.php @@ -0,0 +1,41 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getUserId(): string + { + return $this->userId; + } + + public function setUserId(string $userId) + { + $this->userId = $userId; + } + } +} diff --git a/Frontend/src/Models/Action/DeletePostModel.php b/Frontend/src/Models/Action/DeletePostModel.php new file mode 100644 index 0000000..306bbef --- /dev/null +++ b/Frontend/src/Models/Action/DeletePostModel.php @@ -0,0 +1,41 @@ +postId; + } + + public function setPostId(string $postId) + { + $this->postId = $postId; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/Frontend/src/Models/Action/FollowModel.php b/Frontend/src/Models/Action/FollowModel.php new file mode 100644 index 0000000..0ec132a --- /dev/null +++ b/Frontend/src/Models/Action/FollowModel.php @@ -0,0 +1,26 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + } +} diff --git a/Frontend/src/Models/Action/InviteFeedUserModel.php b/Frontend/src/Models/Action/InviteFeedUserModel.php new file mode 100644 index 0000000..1a9848d --- /dev/null +++ b/Frontend/src/Models/Action/InviteFeedUserModel.php @@ -0,0 +1,56 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setUsername(string $username) + { + $this->username = $username; + } + + public function getRole(): string + { + return $this->role; + } + + public function setRole(string $role) + { + $this->role = $role; + } + } +} diff --git a/Frontend/src/Models/Action/LoginModel.php b/Frontend/src/Models/Action/LoginModel.php new file mode 100644 index 0000000..ef106b2 --- /dev/null +++ b/Frontend/src/Models/Action/LoginModel.php @@ -0,0 +1,41 @@ +loginUser; + } + + public function setLoginUser(string $user) + { + $this->loginUser = $user; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password) + { + $this->password = $password; + } + } +} diff --git a/Frontend/src/Models/Action/RegisterModel.php b/Frontend/src/Models/Action/RegisterModel.php new file mode 100644 index 0000000..9edd020 --- /dev/null +++ b/Frontend/src/Models/Action/RegisterModel.php @@ -0,0 +1,56 @@ +email; + } + + public function setEmail(string $email) + { + $this->email = $email; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setUsername(string $username) + { + $this->username = $username; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password) + { + $this->password = $password; + } + } +} diff --git a/Frontend/src/Models/Action/ResendVerificationModel.php b/Frontend/src/Models/Action/ResendVerificationModel.php new file mode 100644 index 0000000..887707e --- /dev/null +++ b/Frontend/src/Models/Action/ResendVerificationModel.php @@ -0,0 +1,26 @@ +email; + } + + public function setEmail(string $email) + { + $this->email = $email; + } + } +} diff --git a/Frontend/src/Models/Action/UpdateFeedUserRoleModel.php b/Frontend/src/Models/Action/UpdateFeedUserRoleModel.php new file mode 100644 index 0000000..aced34c --- /dev/null +++ b/Frontend/src/Models/Action/UpdateFeedUserRoleModel.php @@ -0,0 +1,56 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getUserId(): string + { + return $this->userId; + } + + public function setUserId(string $userId) + { + $this->userId = $userId; + } + + public function getRole(): string + { + return $this->role; + } + + public function setRole(string $role) + { + $this->role = $role; + } + } +} diff --git a/Frontend/src/Models/Action/UploadModel.php b/Frontend/src/Models/Action/UploadModel.php new file mode 100644 index 0000000..0182c3d --- /dev/null +++ b/Frontend/src/Models/Action/UploadModel.php @@ -0,0 +1,41 @@ +filename; + } + + public function setFilename(string $filename) + { + $this->filename = $filename; + } + + public function getMimeType(): string + { + return $this->mimeType; + } + + public function setMimeType(string $mimeType) + { + $this->mimeType = $mimeType; + } + } +} diff --git a/Frontend/src/Models/ActionModel.php b/Frontend/src/Models/ActionModel.php new file mode 100644 index 0000000..0e1045d --- /dev/null +++ b/Frontend/src/Models/ActionModel.php @@ -0,0 +1,24 @@ +data; + } + + public function setData(array $data) + { + $this->data = $data; + } + } +} diff --git a/Frontend/src/Models/CreatePostPageModel.php b/Frontend/src/Models/CreatePostPageModel.php new file mode 100644 index 0000000..4e286e8 --- /dev/null +++ b/Frontend/src/Models/CreatePostPageModel.php @@ -0,0 +1,24 @@ +feedInfo = $feedInfo; + } + + public function getFeedInfo(): array + { + return $this->feedInfo; + } + } +} diff --git a/Frontend/src/Models/FeedsPageModel.php b/Frontend/src/Models/FeedsPageModel.php new file mode 100644 index 0000000..d18c37d --- /dev/null +++ b/Frontend/src/Models/FeedsPageModel.php @@ -0,0 +1,26 @@ +feeds; + } + + public function setFeeds(PaginatedResult $feeds) + { + $this->feeds = $feeds; + } + } +} diff --git a/Frontend/src/Models/Fragment/FeedPostsFragmentModel.php b/Frontend/src/Models/Fragment/FeedPostsFragmentModel.php new file mode 100644 index 0000000..b935c7a --- /dev/null +++ b/Frontend/src/Models/Fragment/FeedPostsFragmentModel.php @@ -0,0 +1,87 @@ +feedId; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function getLimit(): int + { + return $this->limit; + } + + public function setLimit(int $limit) + { + $this->limit = $limit; + } + + public function getPage(): int + { + return $this->page; + } + + public function setPage(int $page) + { + $this->page = $page; + } + + public function getFeed(): array + { + return $this->feed; + } + + public function setFeed(array $feed) + { + $this->feed = $feed; + } + + public function getPosts(): PaginatedResult + { + return $this->posts; + } + + public function setPosts(PaginatedResult $posts) + { + $this->posts = $posts; + } + } +} diff --git a/Frontend/src/Models/Fragment/HomepagePostsFragmentModel.php b/Frontend/src/Models/Fragment/HomepagePostsFragmentModel.php new file mode 100644 index 0000000..48c3a85 --- /dev/null +++ b/Frontend/src/Models/Fragment/HomepagePostsFragmentModel.php @@ -0,0 +1,57 @@ +limit; + } + + public function setLimit(int $limit) + { + $this->limit = $limit; + } + + public function getPage(): int + { + return $this->page; + } + + public function setPage(int $page) + { + $this->page = $page; + } + + public function getPosts(): PaginatedResult + { + return $this->posts; + } + + public function setPosts(PaginatedResult $posts) + { + $this->posts = $posts; + } + } +} diff --git a/Frontend/src/Models/FragmentModel.php b/Frontend/src/Models/FragmentModel.php new file mode 100644 index 0000000..a5fa3cc --- /dev/null +++ b/Frontend/src/Models/FragmentModel.php @@ -0,0 +1,11 @@ +statusCode; + } + + public function hasStatusCode(): bool + { + return $this->statusCode !== null; + } + + public function setStatusCode(StatusCodeInterface $statusCode) + { + $this->statusCode = $statusCode; + } + + public function getRedirect(): RedirectInterface + { + return $this->redirect; + } + + public function hasRedirect(): bool + { + return $this->redirect !== null; + } + + public function setRedirect(RedirectInterface $redirect) + { + $this->redirect = $redirect; + } + + public function getCrfsToken(): string + { + return $this->crfsToken; + } + + public function setCrfsToken(string $crfsToken) + { + $this->crfsToken = $crfsToken; + } + + public function getUser(): User + { + return $this->user; + } + + public function hasUser(): bool + { + return $this->user !== null; + } + + public function setUser(User $user) + { + $this->user = $user; + } + } +} diff --git a/Frontend/src/Models/HomepageModel.php b/Frontend/src/Models/HomepageModel.php new file mode 100644 index 0000000..96cc718 --- /dev/null +++ b/Frontend/src/Models/HomepageModel.php @@ -0,0 +1,26 @@ +posts; + } + + public function setPosts(PaginatedResult $posts) + { + $this->posts = $posts; + } + } +} diff --git a/Frontend/src/Models/Page/FeedPageModel.php b/Frontend/src/Models/Page/FeedPageModel.php new file mode 100644 index 0000000..bcf2808 --- /dev/null +++ b/Frontend/src/Models/Page/FeedPageModel.php @@ -0,0 +1,40 @@ +feed = $feed; + $this->activeTab = $activeTab; + } + + public function getFeed(): Feed + { + return $this->feed; + } + + public function getActiveTab(): Tab + { + return $this->activeTab; + } + } +} diff --git a/Frontend/src/Models/Page/FeedPeoplePageModel.php b/Frontend/src/Models/Page/FeedPeoplePageModel.php new file mode 100644 index 0000000..2d1949e --- /dev/null +++ b/Frontend/src/Models/Page/FeedPeoplePageModel.php @@ -0,0 +1,47 @@ +feedInvitations; + } + + public function setFeedInvitations(array $feedInvitations) + { + $this->feedInvitations = $feedInvitations; + } + + public function hasFeedUsers(): bool + { + return $this->feedUsers !== null; + } + + public function getFeedUsers(): array + { + return $this->feedUsers; + } + + public function setFeedUsers(array $feedUsers) + { + $this->feedUsers = $feedUsers; + } + } +} diff --git a/Frontend/src/Models/Page/FeedPostsPageModel.php b/Frontend/src/Models/Page/FeedPostsPageModel.php new file mode 100644 index 0000000..82368d5 --- /dev/null +++ b/Frontend/src/Models/Page/FeedPostsPageModel.php @@ -0,0 +1,26 @@ +feedPosts; + } + + public function setFeedPosts(PaginatedResult $feedPosts) + { + $this->feedPosts = $feedPosts; + } + } +} diff --git a/Frontend/src/Models/Page/SearchPageModel.php b/Frontend/src/Models/Page/SearchPageModel.php new file mode 100644 index 0000000..5ef870f --- /dev/null +++ b/Frontend/src/Models/Page/SearchPageModel.php @@ -0,0 +1,74 @@ +searchType = $searchType; + } + + public function getSearchType(): SearchType + { + return $this->searchType; + } + + public function getSearchQuery(): string + { + return $this->searchQuery; + } + + public function setSearchQuery(string $searchQuery) + { + $this->searchQuery = $searchQuery; + } + + public function getSearchResults(): PaginatedResult + { + return $this->searchResults; + } + + public function setSearchResults(PaginatedResult $searchResults) + { + $this->searchResults = $searchResults; + } + + public function getActiveTab(): Tab + { + return $this->activeTab; + } + + public function setActiveTab(Tab $activeTab) + { + $this->activeTab = $activeTab; + } + } +} diff --git a/Frontend/src/Models/PageModel.php b/Frontend/src/Models/PageModel.php new file mode 100644 index 0000000..5fd6ad9 --- /dev/null +++ b/Frontend/src/Models/PageModel.php @@ -0,0 +1,59 @@ +title = $title; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getCanonicalUri(): string + { + return $this->canonicalUri; + } + + public function setCanonicalUri(string $canonicalUri) + { + $this->canonicalUri = $canonicalUri; + } + + public function hasCanonicalUri(): bool + { + return $this->canonicalUri !== null; + } + + public function isTrackingDisabled(): bool + { + return $this->trackingDisabled; + } + + public function disableTracking() + { + $this->trackingDisabled = true; + } + } +} diff --git a/Frontend/src/Models/PostPageModel.php b/Frontend/src/Models/PostPageModel.php new file mode 100644 index 0000000..eaf047b --- /dev/null +++ b/Frontend/src/Models/PostPageModel.php @@ -0,0 +1,24 @@ +post = $post; + } + + public function getPost(): array + { + return $this->post; + } + } +} diff --git a/Frontend/src/Models/StaticPageModel.php b/Frontend/src/Models/StaticPageModel.php new file mode 100644 index 0000000..f6bef31 --- /dev/null +++ b/Frontend/src/Models/StaticPageModel.php @@ -0,0 +1,53 @@ +name = $name; + $this->language = $language; + } + + public function getName(): string + { + return $this->name; + } + + public function getLanguage(): LanguageInterface + { + return $this->language; + } + + public function getStaticPage(): StaticPage + { + return $this->staticPage; + } + + public function setStaticPage(StaticPage $staticPage) + { + $this->staticPage = $staticPage; + } + } +} diff --git a/Frontend/src/Queries/Feed/FetchFeedInvitationsQuery.php b/Frontend/src/Queries/Feed/FetchFeedInvitationsQuery.php new file mode 100644 index 0000000..7ab22c9 --- /dev/null +++ b/Frontend/src/Queries/Feed/FetchFeedInvitationsQuery.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId) + { + return $this->apiGateway->getFeedInvitations($feedId)->unwrap(); + } + } +} diff --git a/Frontend/src/Queries/Feed/FetchFeedUsersQuery.php b/Frontend/src/Queries/Feed/FetchFeedUsersQuery.php new file mode 100644 index 0000000..53534a9 --- /dev/null +++ b/Frontend/src/Queries/Feed/FetchFeedUsersQuery.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId) + { + return $this->apiGateway->getFeedUsers($feedId)->unwrap(); + } + } +} diff --git a/Frontend/src/Queries/Feed/LookupVanityQuery.php b/Frontend/src/Queries/Feed/LookupVanityQuery.php new file mode 100644 index 0000000..76ba8d4 --- /dev/null +++ b/Frontend/src/Queries/Feed/LookupVanityQuery.php @@ -0,0 +1,30 @@ +dataStoreReader = $dataStoreReader; + } + + public function execute(string $vanity) + { + if ($this->dataStoreReader->hasVanity($vanity)) { + return $this->dataStoreReader->getFeedByVanity($vanity); + } + + return $vanity; + } + } +} diff --git a/Frontend/src/Queries/FetchFeedPostsQuery.php b/Frontend/src/Queries/FetchFeedPostsQuery.php new file mode 100644 index 0000000..293835a --- /dev/null +++ b/Frontend/src/Queries/FetchFeedPostsQuery.php @@ -0,0 +1,34 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId, $limit = 20, $page = 1) + { + $response = $this->apiGateway->getFeedPosts($feedId, $limit, $page); + $data = $response->unwrap(); + + if ($data === null) { + return new PaginatedResult([]); + } + + return new PaginatedResult($data); + } + } +} diff --git a/Frontend/src/Queries/FetchFeedQuery.php b/Frontend/src/Queries/FetchFeedQuery.php new file mode 100644 index 0000000..c35d303 --- /dev/null +++ b/Frontend/src/Queries/FetchFeedQuery.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $feedId) + { + return $this->apiGateway->getFeed($feedId)->unwrap(); + } + } +} diff --git a/Frontend/src/Queries/FetchStaticPageQuery.php b/Frontend/src/Queries/FetchStaticPageQuery.php new file mode 100644 index 0000000..dba524d --- /dev/null +++ b/Frontend/src/Queries/FetchStaticPageQuery.php @@ -0,0 +1,28 @@ +dataStoreReader = $dataStoreReader; + } + + public function execute(string $name, LanguageInterface $language): StaticPage + { + return $this->dataStoreReader->getStaticPage($name, $language); + } + } +} diff --git a/Frontend/src/Queries/FetchUserFeedQuery.php b/Frontend/src/Queries/FetchUserFeedQuery.php new file mode 100644 index 0000000..bbfe8bc --- /dev/null +++ b/Frontend/src/Queries/FetchUserFeedQuery.php @@ -0,0 +1,30 @@ +apiGateway = $apiGateway; + } + + public function execute(int $limit = 20, int $page = 1): PaginatedResult + { + $response = $this->apiGateway->getUserFeed($limit, $page); + $data = $response->unwrap(); + + return new PaginatedResult($data); + } + } +} diff --git a/Frontend/src/Queries/FetchUserFeedsQuery.php b/Frontend/src/Queries/FetchUserFeedsQuery.php new file mode 100644 index 0000000..3482487 --- /dev/null +++ b/Frontend/src/Queries/FetchUserFeedsQuery.php @@ -0,0 +1,30 @@ +apiGateway = $apiGateway; + } + + public function execute(int $limit = 20, int $page = 1): PaginatedResult + { + $response = $this->apiGateway->getUserFeeds($limit, $page); + $data = $response->unwrap(); + + return new PaginatedResult($data); + } + } +} diff --git a/Frontend/src/Queries/IsLoggedInQuery.php b/Frontend/src/Queries/IsLoggedInQuery.php new file mode 100644 index 0000000..974026b --- /dev/null +++ b/Frontend/src/Queries/IsLoggedInQuery.php @@ -0,0 +1,26 @@ +session = $session; + } + + public function execute(): bool + { + return $this->session->hasUser(); + } + } +} diff --git a/Frontend/src/Queries/Post/FetchPostQuery.php b/Frontend/src/Queries/Post/FetchPostQuery.php new file mode 100644 index 0000000..ce2dc32 --- /dev/null +++ b/Frontend/src/Queries/Post/FetchPostQuery.php @@ -0,0 +1,26 @@ +apiGateway = $apiGateway; + } + + public function execute(string $postId) + { + return $this->apiGateway->getPost($postId)->unwrap(); + } + } +} diff --git a/Frontend/src/Queries/SearchQuery.php b/Frontend/src/Queries/SearchQuery.php new file mode 100644 index 0000000..07c8a9f --- /dev/null +++ b/Frontend/src/Queries/SearchQuery.php @@ -0,0 +1,29 @@ +apiGateway = $apiGateway; + } + + public function execute(string $query, string $type): PaginatedResult + { + $data = $this->apiGateway->search($query, $type)->unwrap(); + + return new PaginatedResult($data); + } + } +} diff --git a/Frontend/src/Renderers/FeedPageRenderer.php b/Frontend/src/Renderers/FeedPageRenderer.php new file mode 100644 index 0000000..2d9fb7b --- /dev/null +++ b/Frontend/src/Renderers/FeedPageRenderer.php @@ -0,0 +1,82 @@ +feedHeaderSnippet = $feedHeaderSnippet; + $this->feedButtonsSnippet = $feedButtonsSnippet; + $this->invitationBannerSnippet = $invitationBannerSnippet; + $this->feedNavigationSnippet = $feedNavigationSnippet; + } + + public function render(AbstractModel $model): string + { + /** @var FeedPostsPageModel $model */ + + $feed = $model->getFeed(); + $template = $this->getTemplate(); + $main = $template->getMainElement(); + + if ($feed->hasPostAccess()) { + $main->appendChild($this->feedButtonsSnippet->render($template, $feed->getId())); + } + + if ($feed->isUserInvited()) { + $main->appendChild($this->invitationBannerSnippet->render($template)); + } + + $main->appendChild($this->feedHeaderSnippet->render($template, $feed)); + $main->appendChild($this->feedNavigationSnippet->render($template, $model->getActiveTab(), $feed)); + + return parent::render($model); + } + } +} diff --git a/Frontend/src/Renderers/Fragment/FeedPostsFragmentRenderer.php b/Frontend/src/Renderers/Fragment/FeedPostsFragmentRenderer.php new file mode 100644 index 0000000..a7c25cc --- /dev/null +++ b/Frontend/src/Renderers/Fragment/FeedPostsFragmentRenderer.php @@ -0,0 +1,44 @@ +postSnippet = $postSnippet; + } + + public function render(FragmentModel $model): string + { + /** @var FeedPostsFragmentModel $model */ + + $document = new Dom\Document; + $fragment = $document->createDocumentFragment(); + + $feed = $model->getFeed(); + + foreach ($model->getPosts() as $post) { + $post['rendered_body'] = $post['preview']; + + $fragment->appendChild( + $this->postSnippet->render($document, $post, $feed) + ); + } + + return $document->saveHTML($fragment); + } + } +} diff --git a/Frontend/src/Renderers/Fragment/FragmentRenderer.php b/Frontend/src/Renderers/Fragment/FragmentRenderer.php new file mode 100644 index 0000000..835ea95 --- /dev/null +++ b/Frontend/src/Renderers/Fragment/FragmentRenderer.php @@ -0,0 +1,13 @@ +postSnippet = $postSnippet; + } + + public function render(FragmentModel $model): string + { + /** @var HomepagePostsFragmentModel $model */ + + $document = new Dom\Document; + $fragment = $document->createDocumentFragment(); + + foreach ($model->getPosts() as $post) { + $post['rendered_body'] = $post['preview']; + + $fragment->appendChild( + $this->postSnippet->render($document, $post, $post['feed']) + ); + } + + return $document->saveHTML($fragment); + } + } +} diff --git a/Frontend/src/Renderers/Page/Account/VerifyAccountPageRenderer.php b/Frontend/src/Renderers/Page/Account/VerifyAccountPageRenderer.php new file mode 100644 index 0000000..50c703f --- /dev/null +++ b/Frontend/src/Renderers/Page/Account/VerifyAccountPageRenderer.php @@ -0,0 +1,45 @@ +domBackend = $domBackend; + } + + public function render(PageModel $model, Document $template) + { + $fileName = 'templates://content/verify/success.html'; + + if ($model->hasStatusCode() && $model->getStatusCode() instanceof NotFound) { + $fileName = 'templates://content/verify/error.html'; + } + + $content = $this->domBackend->loadHtml($fileName); + $contentElement = $template->importDocument($content); + + $template->getMainElement()->appendChild($contentElement); + + $header = $template->queryOne('//header[@class="page-header"]'); + + if ($header !== null) { + $header->parentNode->removeChild($header); + } + } + } +} diff --git a/Frontend/src/Renderers/Page/CreatePostPageRenderer.php b/Frontend/src/Renderers/Page/CreatePostPageRenderer.php new file mode 100644 index 0000000..3e53032 --- /dev/null +++ b/Frontend/src/Renderers/Page/CreatePostPageRenderer.php @@ -0,0 +1,178 @@ +feedButtonsSnippet = $feedButtonsSnippet; + $this->iconSnippet = $iconSnippet; + $this->uriBuilder = $uriBuilder; + } + + public function render(PageModel $model, Document $template) + { + /** @var CreatePostPageModel $model */ + + $main = $template->getMainElement(); + $feed = $model->getFeedInfo(); + $feedId = $feed['id']; + + $main->appendChild($this->feedButtonsSnippet->render($template, $feedId)); + + $wrapperElement = $template->createElement('div'); + $wrapperElement->setClassName('page-wrapper -padding'); + + $main->appendChild($wrapperElement); + + $formElement = $template->createElement('form'); + $formElement->setAttribute('is', 'ajax-form'); + $formElement->setAttribute('method', 'post'); + $formElement->setAttribute('action', '/action/note/create'); + $wrapperElement->appendChild($formElement); + + $cardElement = $template->createElement('file-drop'); + $cardElement->setClassName('post-card'); + $cardElement->setAttribute('append-to', '.attachments'); + $cardElement->setAttribute('file-element', 'post-attachment'); + $formElement->appendChild($cardElement); + + $headerElement = $template->createElement('header'); + $headerElement->setClassName('header'); + $cardElement->appendChild($headerElement); + + /*$authorElement = $template->createElement('div'); + $authorElement->setClassName('author'); + $headerElement->appendChild($authorElement); + + $authorTextElement = $template->createElement('div'); + $authorTextElement->setClassName('text'); + $authorTextElement->appendText($model->getUser()->getDisplayName()); + $authorElement->appendChild($authorTextElement);*/ + + $timeElement = $template->createElement('local-time'); + $timeElement->setClassName('time'); + $timeElement->setAttribute('month', 'long'); + $timeElement->setAttribute('day', 'numeric'); + $timeElement->setAttribute('year', 'numeric'); + $timeElement->setAttribute('datetime', date('c')); + $timeElement->appendText(date('d.m.Y')); + $headerElement->appendChild($timeElement); + + $bodyElement = $template->createElement('div'); + $bodyElement->setClassName('body'); + $cardElement->appendChild($bodyElement); + + $titleInputElement = $template->createElement('input'); + $titleInputElement->setAttribute('is', 'validated-input'); + $titleInputElement->setClassName('title'); + $titleInputElement->setAttribute('placeholder', $this->getTranslator()->translate('Title')); + $titleInputElement->setAttribute('name', 'title'); + $titleInputElement->setAttribute('required', ''); + $titleInputElement->setAttribute('max-byte-size', '64'); + $titleInputElement->setAttribute('autocomplete', 'off'); + $bodyElement->appendChild($titleInputElement); + + $subtitle = $template->createElement('div'); + $subtitle->setClassName('subtitle'); + $bodyElement->appendChild($subtitle); + + $subtitle->appendText($model->getUser()->getDisplayName()); + $subtitle->appendText(' • '); + + $feedLink = $template->createElement('a'); + $feedLink->setClassName('basic-link'); + $feedLink->setAttribute('href', $this->uriBuilder->buildFeedPageUri($feed['id'])); + $feedLink->appendText($feed['name']); + $subtitle->appendChild($feedLink); + + $textareaElement = $template->createElement('textarea'); + $textareaElement->setClassName('textarea'); + $textareaElement->setAttribute('is', 'auto-textarea'); + $textareaElement->setAttribute('name', 'body'); + $textareaElement->setAttribute('max-size', '8192'); + $textareaElement->setAttribute('placeholder', $this->getTranslator()->translate('Write something...')); + $bodyElement->appendChild($textareaElement); + + $attachmentsElement = $template->createElement('div'); + $attachmentsElement->setClassName('attachments'); + $cardElement->appendChild($attachmentsElement); + + $formError = $template->createElement('form-error'); + $cardElement->appendChild($formError); + + $buttonsElement = $template->createElement('div'); + $buttonsElement->setClassName('buttons'); + $cardElement->appendChild($buttonsElement); + + $postButton = $template->createElement('button'); + $postButton->setClassName('light-button -color'); + $postButton->setAttribute('type', 'submit'); + $postButton->setAttribute('disabled', ''); + $buttonsElement->appendChild($postButton); + + $postButtonInner = $template->createElement('span'); + $postButtonInner->setClassName('inner'); + $postButton->appendChild($postButtonInner); + + $postButtonInner->appendChild($this->iconSnippet->render($template, 'actions/post', 'icon')); + + $postButtonText = $template->createElement('span'); + $postButtonText->setClassName('label'); + $postButtonInner->appendChild($postButtonText); + + $postButtonText->appendText($this->getTranslator()->translate('Post to')); + $postButtonText->appendText(' '); + + $postButtonFeedName = $template->createElement('span'); + $postButtonFeedName->setClassName('name'); + $postButtonFeedName->appendText($feed['name']); + $postButtonText->appendChild($postButtonFeedName); + + $uploadMessage = $template->createElement('div'); + $uploadMessage->setClassName('post-card-outside-text'); + $uploadMessage->appendText($this->getTranslator()->translate('Attach files by dragging & dropping or') . ' '); + $formElement->appendChild($uploadMessage); + + $uploadButton = $template->createElement('file-pick'); + $uploadButton->setClassName('basic-link'); + $uploadButton->appendText($this->getTranslator()->translate('selecting them')); + $uploadMessage->appendChild($uploadButton); + + $feedElement = $template->createElement('input'); + $feedElement->setAttribute('type', 'hidden'); + $feedElement->setAttribute('name', 'feed_id'); + $feedElement->setAttribute('value', $feedId); + $formElement->appendChild($feedElement); + } + } +} diff --git a/Frontend/src/Renderers/Page/Feed/FeedPeoplePageRenderer.php b/Frontend/src/Renderers/Page/Feed/FeedPeoplePageRenderer.php new file mode 100644 index 0000000..383e166 --- /dev/null +++ b/Frontend/src/Renderers/Page/Feed/FeedPeoplePageRenderer.php @@ -0,0 +1,142 @@ +userRolesOptionsSnippet = $userRolesOptionsSnippet; + $this->feedUserCardSnippet = $feedUserCardSnippet; + $this->feedInvitationCardSnippet = $feedInvitationCardSnippet; + $this->iconButtonSnippet = $iconButtonSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var \Timetabio\Frontend\Models\Page\FeedPeoplePageModel $model */ + + $feed = $model->getFeed(); + $main = $template->getMainElement(); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding'); + $main->appendChild($wrapper); + + if ($feed->hasUsersManageAccess()) { + $wrapper->appendChild($this->renderInvitations($template, $model)); + } + + if (!$model->hasFeedUsers()) { + return; + } + + $usersTitle = $template->createElement('h2'); + $usersTitle->setClassName('basic-heading-b _margin-after-s'); + $usersTitle->appendText($this->getTranslator()->translate('Users')); + $wrapper->appendChild($usersTitle); + + $usersList = $template->createElement('ul'); + $usersList->setClassName('user-list'); + $wrapper->appendChild($usersList); + + foreach ($model->getFeedUsers() as $user) { + $usersList->appendChild($this->feedUserCardSnippet->render($template, $user, $feed)); + } + } + + private function renderInvitations(Document $template, FeedPeoplePageModel $model) + { + $feed = $model->getFeed(); + $fragment = $template->createDocumentFragment(); + + $invitationsTitle = $template->createElement('h2'); + $invitationsTitle->setClassName('basic-heading-b _margin-after-s'); + $invitationsTitle->appendText($this->getTranslator()->translate('Invitations')); + $fragment->appendChild($invitationsTitle); + + $invitationForm = $template->createElement('form'); + $invitationForm->setClassName('user-list-item _margin-after-s'); + $invitationForm->setAttribute('is', 'ajax-form'); + $invitationForm->setAttribute('method', 'post'); + $invitationForm->setAttribute('action', '/action/feed/invite-user'); + $invitationForm->setAttribute('autocomplete', 'off'); + $fragment->appendChild($invitationForm); + + $invitationUsername = $template->createElement('input'); + $invitationUsername->setClassName('name'); + $invitationUsername->setAttribute('name', 'username'); + $invitationUsername->setAttribute('required', ''); + $invitationUsername->setAttribute('placeholder', 'peanut-butter'); + $invitationForm->appendChild($invitationUsername); + + $invitationRoleSelect = $template->createElement('select'); + $invitationRoleSelect->setClassName('role light-select'); + $invitationRoleSelect->setAttribute('name', 'role'); + $invitationRoleSelect->setAttribute('required', ''); + $invitationForm->appendChild($invitationRoleSelect); + + $invitationRoleSelect->appendChild($this->userRolesOptionsSnippet->render($template)); + + $feedIdInput = $template->createElement('input'); + $feedIdInput->setAttribute('type', 'hidden'); + $feedIdInput->setAttribute('name', 'feed_id'); + $feedIdInput->setAttribute('value', $feed->getId()); + $invitationForm->appendChild($feedIdInput); + + $invitationButton = $this->iconButtonSnippet->render($template, 'actions/invite', 'Invite', '-color'); + $invitationButton->setAttribute('type', 'submit'); + $invitationButton->setAttribute('disabled', ''); + $invitationForm->appendChild($invitationButton); + + $invitationsList = $template->createElement('ul'); + $invitationsList->setClassName('user-list _margin-after-l'); + $fragment->appendChild($invitationsList); + + foreach ($model->getFeedInvitations() as $invitation) { + $invitationsList->appendChild($this->feedInvitationCardSnippet->render($template, $invitation, $feed)); + } + + return $fragment; + } + } +} diff --git a/Frontend/src/Renderers/Page/FeedPageRenderer.php b/Frontend/src/Renderers/Page/FeedPageRenderer.php new file mode 100644 index 0000000..85149d6 --- /dev/null +++ b/Frontend/src/Renderers/Page/FeedPageRenderer.php @@ -0,0 +1,66 @@ +postSnippet = $postSnippet; + $this->paginationButtonSnippet = $paginationButtonSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var FeedPostsPageModel $model */ + + $feed = $model->getFeed(); + $posts = $model->getFeedPosts(); + $main = $template->getMainElement(); + + $wrapper = $template->createElement('paginated-view'); + $wrapper->setClassName('page-wrapper -padding'); + $wrapper->setAttribute('endpoint-uri', '/fragment/feed-posts/' . $feed->getId()); + $wrapper->setAttribute('total-pages', $posts->getPages()); + + $main->appendChild($wrapper); + + $postsElement = $template->createElement('paginated-list'); + $postsElement->setClassName('post-list'); + + $wrapper->appendChild($postsElement); + + foreach ($posts as $post) { + $post['rendered_body'] = $post['preview']; + + $postsElement->appendChild( + $this->postSnippet->render($template, $post, $feed->toArray()) + ); + } + + $wrapper->appendChild($this->paginationButtonSnippet->render($template, $posts)); + } + } +} diff --git a/Frontend/src/Renderers/Page/FeedsPageRenderer.php b/Frontend/src/Renderers/Page/FeedsPageRenderer.php new file mode 100644 index 0000000..44c2316 --- /dev/null +++ b/Frontend/src/Renderers/Page/FeedsPageRenderer.php @@ -0,0 +1,106 @@ +feedCardSnippet = $feedCardSnippet; + $this->paginationButtonSnippet = $paginationButtonSnippet; + $this->homepageNavigationSnippet = $homepageNavigationSnippet; + $this->homepageOnboardingSnippet = $homepageOnboardingSnippet; + $this->floatingButtonSnippet = $floatingButtonSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var FeedsPageModel $model */ + + $feeds = $model->getFeeds(); + $main = $template->getMainElement(); + + $floatingButtons = $template->createElement('nav'); + $floatingButtons->setClassName('floating-buttons'); + $main->appendChild($floatingButtons); + + $floatingButtons->appendChild($this->floatingButtonSnippet->render($template, 'feed', '/account/feeds/new', 'New Feed')); + + $main->appendChild($this->homepageNavigationSnippet->render($template, new \Timetabio\Frontend\Tabs\Homepage\Feeds)); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding -no-padding-top'); + $main->appendChild($wrapper); + + if ($feeds->getTotal() === 0) { + $wrapper->appendChild($this->homepageOnboardingSnippet->render($template)); + return; + } + + $listElement = $template->createElement('paginated-view'); + $listElement->setAttribute('endpoint-uri', '/fragment/user-feeds'); + $listElement->setAttribute('total-pages', $feeds->getPages()); + $wrapper->appendChild($listElement); + + $feedsElement = $template->createElement('paginated-list'); + $feedsElement->setClassName('post-list -smaller-margin'); + $listElement->appendChild($feedsElement); + + foreach ($feeds as $feed) { + $feedsElement->appendChild( + $this->feedCardSnippet->render($template, $feed) + ); + } + + $listElement->appendChild($this->paginationButtonSnippet->render($template, $feeds)); + } + } +} diff --git a/Frontend/src/Renderers/Page/HomepageRenderer.php b/Frontend/src/Renderers/Page/HomepageRenderer.php new file mode 100644 index 0000000..1ce67f3 --- /dev/null +++ b/Frontend/src/Renderers/Page/HomepageRenderer.php @@ -0,0 +1,95 @@ +postSnippet = $postSnippet; + $this->paginationButtonSnippet = $paginationButtonSnippet; + $this->homepageNavigationSnippet = $homepageNavigationSnippet; + $this->homepageOnboardingSnippet = $homepageOnboardingSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var HomepageModel $model */ + + $posts = $model->getPosts(); + $main = $template->getMainElement(); + + $main->appendChild($this->homepageNavigationSnippet->render($template, new \Timetabio\Frontend\Tabs\Homepage\Posts)); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding -no-padding-top'); + $main->appendChild($wrapper); + + if ($posts->getTotal() === 0) { + $wrapper->appendChild($this->homepageOnboardingSnippet->render($template)); + return; + } + + $listElement = $template->createElement('paginated-view'); + $listElement->setAttribute('endpoint-uri', '/fragment/homepage-posts'); + $listElement->setAttribute('total-pages', $posts->getPages()); + $wrapper->appendChild($listElement); + + $postsElement = $template->createElement('paginated-list'); + $postsElement->setClassName('post-list'); + $listElement->appendChild($postsElement); + + foreach ($posts as $post) { + // TODO: this needs changing + $post['rendered_body'] = $post['preview']; + + $postsElement->appendChild( + $this->postSnippet->render($template, $post, $post['feed']) + ); + } + + $listElement->appendChild($this->paginationButtonSnippet->render($template, $posts)); + } + } +} diff --git a/Frontend/src/Renderers/Page/PageRendererInterface.php b/Frontend/src/Renderers/Page/PageRendererInterface.php new file mode 100644 index 0000000..ac0a56b --- /dev/null +++ b/Frontend/src/Renderers/Page/PageRendererInterface.php @@ -0,0 +1,14 @@ +postSnippet = $postSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var PostPageModel $model */ + + $post = $model->getPost(); + $main = $template->getMainElement(); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding'); + + $wrapper->appendChild( + $this->postSnippet->render($template, $post, $post['feed']) + ); + + $main->appendChild($wrapper); + } + } +} diff --git a/Frontend/src/Renderers/Page/SearchPageRenderer.php b/Frontend/src/Renderers/Page/SearchPageRenderer.php new file mode 100644 index 0000000..1b0714e --- /dev/null +++ b/Frontend/src/Renderers/Page/SearchPageRenderer.php @@ -0,0 +1,96 @@ +postSnippet = $postSnippet; + $this->feedCardSnippet = $feedCardSnippet; + $this->searchTabNavSnippet = $searchTabNavSnippet; + } + + public function render(PageModel $model, Document $template) + { + /** @var SearchPageModel $model */ + + $query = $model->getSearchQuery(); + $main = $template->getMainElement(); + + $searchInput = $template->queryOne('//input[@name="q"]'); + $searchInput->setAttribute('value', $query); + + $navElement = $this->searchTabNavSnippet->render( + $template, + $model->getActiveTab(), + $query + ); + + $navElement->setClassName('tab-nav -margin'); + $main->appendChild($navElement); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding -no-padding-top'); + $main->appendChild($wrapper); + + $postsElement = $template->createElement('div'); + $postsElement->setClassName('post-list'); + $wrapper->appendChild($postsElement); + + foreach ($model->getSearchResults() as $result) { + $postsElement->appendChild( + $this->renderResult($template, $result) + ); + } + } + + private function renderResult(Document $template, array $result): Element + { + switch ($result['type']) { + case 'post': + return $this->renderPostCard($template, $result['data']); + case 'feed': + return $this->feedCardSnippet->render($template, $result['data']); + } + + throw new \RuntimeException('invalid result type'); + } + + private function renderPostCard(Document $template, array $post): Element + { + $post['rendered_body'] = $post['preview']; + + return $this->postSnippet->render($template, $post, $post['feed']); + } + } +} diff --git a/Frontend/src/Renderers/Page/StaticPageRenderer.php b/Frontend/src/Renderers/Page/StaticPageRenderer.php new file mode 100644 index 0000000..d627066 --- /dev/null +++ b/Frontend/src/Renderers/Page/StaticPageRenderer.php @@ -0,0 +1,44 @@ +domBackend = $domBackend; + } + + public function render(PageModel $model, Document $template) + { + /** @var StaticPageModel $model */ + + $staticPage = $model->getStaticPage(); + + $content = new Document; + $content->loadHTML($staticPage->getContent(), LIBXML_HTML_NOIMPLIED); + + $header = $template->queryOne('//header[@class="page-header"]'); + + $main = $template->queryOne('//main'); + + $main->appendChild($template->importNode($content->documentElement, true)); + + if (!$staticPage->getShowHeader() && $header) { + $header->parentNode->removeChild($header); + } + } + } +} diff --git a/Frontend/src/Renderers/PageRenderer.php b/Frontend/src/Renderers/PageRenderer.php new file mode 100644 index 0000000..47bac8e --- /dev/null +++ b/Frontend/src/Renderers/PageRenderer.php @@ -0,0 +1,56 @@ +template = $template; + $this->pageRenderer = $pageRenderer; + $this->transformer = $transformer; + } + + public function render(AbstractModel $model): string + { + /** @var PageModel $model */ + + $this->pageRenderer->render($model, $this->template); + $this->transformer->apply($model, $this->template); + + return $this->template->saveHTML(); + } + + protected function getTemplate(): Document + { + return $this->template; + } + } +} diff --git a/Frontend/src/Renderers/Renderer.php b/Frontend/src/Renderers/Renderer.php new file mode 100644 index 0000000..c135e73 --- /dev/null +++ b/Frontend/src/Renderers/Renderer.php @@ -0,0 +1,13 @@ +setAttribute('is', 'ajax-button'); + $button->setAttribute('post-uri', $uri); + $button->setAttribute('post-data', json_encode($data)); + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedButtonsSnippet.php b/Frontend/src/Renderers/Snippet/FeedButtonsSnippet.php new file mode 100644 index 0000000..6745305 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedButtonsSnippet.php @@ -0,0 +1,64 @@ +floatingButtonSnippet = $floatingButtonSnippet; + $this->uriBuilder = $uriBuilder; + } + + public function render(Dom\Document $document, string $feedId): Dom\Element + { + $newPostUri = $this->uriBuilder->buildNewPostPageUri($feedId); + + $navElement = $document->createElement('nav'); + $navElement->setClassName('floating-buttons'); + + $navElement->appendChild( + $this->renderButton($document, 'note', $newPostUri, 'Note') + ); + + $navElement->appendChild( + $this->renderButton($document, 'task', $newPostUri, 'Task') + ); + + $navElement->appendChild( + $this->renderButton($document, 'event', $newPostUri, 'Event') + ); + + return $navElement; + } + + private function renderButton(Dom\Document $document, string $icon, string $link, string $label): Dom\Element + { + return $this->floatingButtonSnippet->render( + $document, + $icon, + $link, + $this->getTranslator()->translate($label) + ); + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedCardSnippet.php b/Frontend/src/Renderers/Snippet/FeedCardSnippet.php new file mode 100644 index 0000000..4ea0158 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedCardSnippet.php @@ -0,0 +1,61 @@ +uriBuilder = $uriBuilder; + $this->iconSnippet = $iconSnippet; + } + + public function render(Dom\Document $document, array $feed): Dom\Element + { + $cardElement = $document->createElement('article'); + $cardElement->setClassName('feed-card'); + + $icon = 'public'; + + if ($feed['is_private']) { + $icon = 'private'; + } + + $nameElement = $document->createElement('h2'); + $nameElement->setClassName('name'); + $cardElement->appendChild($nameElement); + + $nameElement->appendChild($this->iconSnippet->render($document, $icon, 'icon')); + + $linkElement = $document->createElement('a'); + $linkElement->setClassName('basic-link -no-bold'); + $linkElement->setAttribute('href', $this->uriBuilder->buildFeedPageUri($feed['id'])); + $linkElement->appendText($feed['name']); + $nameElement->appendChild($linkElement); + + if (!empty($feed['description'])) { + $descriptionElement = $document->createElement('p'); + $descriptionElement->setClassName('description'); + $descriptionElement->appendText($feed['description']); + $cardElement->appendChild($descriptionElement); + } + + return $cardElement; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedHeaderSnippet.php b/Frontend/src/Renderers/Snippet/FeedHeaderSnippet.php new file mode 100644 index 0000000..985cb53 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedHeaderSnippet.php @@ -0,0 +1,80 @@ +iconSnippet = $iconSnippet; + } + + public function render(Dom\Document $template, Feed $feed): Dom\Element + { + $translator = $this->getTranslator(); + + $header = $template->createElement('header'); + $header->setClassName('feed-header'); + + $title = $template->createElement('div'); + $title->setClassName('title'); + + $header->appendChild($title); + + $text = $template->createElement('h1'); + $text->setClassName('text'); + $text->appendText($feed->getName()); + + $title->appendChild($text); + + if ($feed->isVerified()) { + $verifiedIcon = $this->iconSnippet->render($template, 'verified', 'verified'); + $title->appendChild($verifiedIcon); + } + + if ($feed->hasDescription()) { + $description = $template->createElement('div'); + $description->setClassName('description'); + $description->appendText($feed->getDescription()); + $header->appendChild($description); + } + + $follow = $template->createElement('button'); + $follow->setClassName('button basic-button -small'); + $follow->setAttribute('is', 'ajax-button'); + $follow->setAttribute('post-data', json_encode(['feed_id' => $feed->getId()])); + + $label = 'Follow'; + $followUri = '/action/follow'; + + if ($feed->hasUserAdded()) { + $label = 'Unfollow'; + $followUri = '/action/unfollow'; + } + + $follow->setAttribute('post-uri', $followUri); + $follow->appendText($translator->translate($label)); + + if (!$feed->hasUserAdded()) { + $header->appendChild($follow); + } + + return $header; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedInvitationBannerSnippet.php b/Frontend/src/Renderers/Snippet/FeedInvitationBannerSnippet.php new file mode 100644 index 0000000..e22c6f4 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedInvitationBannerSnippet.php @@ -0,0 +1,25 @@ +createElement('div'); + + $banner->setClassName('page-banner'); + $banner->appendText($this->getTranslator()->translate('You have been invited to this feed. Add to gain extended access.')); + + return $banner; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedInvitationCardSnippet.php b/Frontend/src/Renderers/Snippet/FeedInvitationCardSnippet.php new file mode 100644 index 0000000..7d8a9f7 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedInvitationCardSnippet.php @@ -0,0 +1,68 @@ +iconButtonSnippet = $iconButtonSnippet; + $this->ajaxButtonSnippet = $ajaxButtonSnippet; + $this->userRoleLocator = $userRoleLocator; + } + + public function render(Document $document, array $invitation, Feed $feed): Element + { + $role = $this->userRoleLocator->locate($invitation['role']); + + $invitationItem = $document->createElement('li'); + $invitationItem->setClassName('user-list-item'); + + $invitationName = $document->createElement('span'); + $invitationName->setClassName('name'); + $invitationName->appendText(new DisplayName($invitation['user'])); + $invitationItem->appendChild($invitationName); + + $invitationRole = $document->createElement('span'); + $invitationRole->setClassName('role light-pill'); + $invitationRole->appendText($role->getLabel()); + $invitationItem->appendChild($invitationRole); + + $invitationButton = $this->iconButtonSnippet->render($document, 'actions/delete', 'Delete'); + + $this->ajaxButtonSnippet->render($invitationButton, '/action/feed/delete-invitation', [ + 'feed_id' => $feed->getId(), + 'user_id' => $invitation['user']['id'] + ]); + + $invitationItem->appendChild($invitationButton); + + return $invitationItem; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedListSnippet.php b/Frontend/src/Renderers/Snippet/FeedListSnippet.php new file mode 100644 index 0000000..5f0072b --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedListSnippet.php @@ -0,0 +1,61 @@ +uriBuilder = $uriBuilder; + } + + public function render(Document $template, PaginatedResult $feeds): Element + { + $feedsElement = $template->createElement('nav'); + $feedsElement->setClassName('feed-list _margin-after'); + + foreach ($feeds as $feed) { + $feedsElement->appendChild($this->renderFeed($template, $feed)); + } + + return $feedsElement; + } + + private function renderFeed(Document $template, array $feed): Element + { + $feedLink = $template->createElement('a'); + $feedLink->setClassName('item'); + $feedLink->setAttribute('href', $this->uriBuilder->buildFeedPageUri($feed['id'])); + + $feedIcon = $template->createElement('svg'); + $feedIcon->setClassName('icon'); + $feedLink->appendChild($feedIcon); + + $iconName = 'public'; + + if ($feed['is_private']) { + $iconName = 'private'; + } + + $useElement = $template->createElement('use'); + $useElement->setAttribute('xlink:href', '/icons/' . $iconName . '.svg#icon'); + $feedIcon->appendChild($useElement); + + $feedLink->appendText($feed['name']); + + return $feedLink; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedNavigationSnippet.php b/Frontend/src/Renderers/Snippet/FeedNavigationSnippet.php new file mode 100644 index 0000000..8eda20b --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedNavigationSnippet.php @@ -0,0 +1,56 @@ +tabNavSnippet = $tabNavSnippet; + $this->uriBuilder = $uriBuilder; + } + + public function render(Dom\Document $document, Tab $current, Feed $feed): Dom\Element + { + $feedId = $feed->getId(); + + $items = [ + new Posts($this->uriBuilder->buildFeedPageUri($feedId)), + new People($this->uriBuilder->buildFeedPeoplePageUri($feedId)) + ]; + + if ($feed->hasUserAdded()) { + $items[] = new Settings($this->uriBuilder->buildFeedOptionsPageUri($feedId)); + } + + $nav = $this->tabNavSnippet->render($document, $current, ...$items); + $nav->setClassName('tab-nav -responsive'); + + return $nav; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FeedUserCardSnippet.php b/Frontend/src/Renderers/Snippet/FeedUserCardSnippet.php new file mode 100644 index 0000000..47b67b9 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FeedUserCardSnippet.php @@ -0,0 +1,107 @@ +iconButtonSnippet = $iconButtonSnippet; + $this->userRolesOptionsSnippet = $userRolesOptionsSnippet; + $this->ajaxButtonSnippet = $ajaxButtonSnippet; + $this->userRoleLocator = $userRoleLocator; + } + + public function render(Document $document, array $user, Feed $feed): Element + { + $userItem = $document->createElement('li'); + $userItem->setClassName('user-list-item'); + + $userName = $document->createElement('span'); + $userName->setClassName('name'); + $userName->appendText(new DisplayName($user['user'])); + $userItem->appendChild($userName); + + $userItem->appendChild($this->renderRole($document, $user, $feed)); + + if ($feed->hasUsersManageAccess()) { + $userItem->appendChild($this->renderDeleteButton($document, $user, $feed)); + } + + return $userItem; + } + + private function renderRole(Document $document, array $user, Feed $feed): Element + { + $role = $this->userRoleLocator->locate($user['role']); + + // TODO: change this condition ($role instanceof Owner) + if (!$feed->hasUsersManageAccess() || !$user['meta']['is_modifiable']) { + $pill = $document->createElement('span'); + + $pill->setClassName('role light-pill'); + $pill->appendText($role->getLabel()); + + return $pill; + } + + $userRoleSelect = $document->createElement('select'); + $userRoleSelect->setAttribute('is', 'ajax-select'); + $userRoleSelect->setAttribute('post-uri', '/action/feed/update-user'); + $userRoleSelect->setAttribute('post-data', json_encode([ + 'feed_id' => $feed->getId(), + 'user_id' => $user['user']['id'] + ])); + $userRoleSelect->setClassName('role light-select'); + $userRoleSelect->setAttribute('name', 'role'); + $userRoleSelect->appendChild($this->userRolesOptionsSnippet->render($document, $user['role'])); + + return $userRoleSelect; + } + + private function renderDeleteButton(Document $document, array $user, Feed $feed): Element + { + $deleteButton = $this->iconButtonSnippet->render($document, 'actions/delete', 'Delete'); + + $this->ajaxButtonSnippet->render($deleteButton, '/action/feed/delete-user', [ + 'feed_id' => $feed->getId(), + 'user_id' => $user['user']['id'] + ]); + + if (!$user['meta']['is_modifiable']) { + $deleteButton->setAttribute('disabled', ''); + } + + return $deleteButton; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/FloatingButtonSnippet.php b/Frontend/src/Renderers/Snippet/FloatingButtonSnippet.php new file mode 100644 index 0000000..c6be0d2 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/FloatingButtonSnippet.php @@ -0,0 +1,42 @@ +iconSnippet = $iconSnippet; + } + + public function render(Document $document, string $icon, string $link, string $label): Element + { + $linkElement = $document->createElement('a'); + + $linkElement->setClassName('floating-button'); + $linkElement->setAttribute('href', $link); + $linkElement->setAttribute('role', 'button'); + $linkElement->setAttribute('title', $label); + + $linkElement->appendChild($this->iconSnippet->render($document, $icon, 'icon')); + + $labelElement = $document->createElement('span'); + $labelElement->setClassName('label'); + $labelElement->appendText($label); + + $linkElement->appendChild($labelElement); + + return $linkElement; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/HomepageNavigationSnippet.php b/Frontend/src/Renderers/Snippet/HomepageNavigationSnippet.php new file mode 100644 index 0000000..26828cb --- /dev/null +++ b/Frontend/src/Renderers/Snippet/HomepageNavigationSnippet.php @@ -0,0 +1,38 @@ +tabNavSnippet = $tabNavSnippet; + } + + public function render(Document $document, Tab $current): Element + { + $nav = $this->tabNavSnippet->render($document, $current, new Posts, new Feeds); + + $nav->setClassName('tab-nav -margin'); + + return $nav; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/HomepageOnboardingSnippet.php b/Frontend/src/Renderers/Snippet/HomepageOnboardingSnippet.php new file mode 100644 index 0000000..7d0ac63 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/HomepageOnboardingSnippet.php @@ -0,0 +1,36 @@ +createElement('div'); + $card->setClassName('generic-card -center'); + + $title = $document->createElement('h2'); + $title->setClassName('basic-heading-a _margin-after-xs'); + $title->appendText('Welcome to timetab.io'); + $card->appendChild($title); + + $text = $document->createElement('p'); + $text->setClassName('_margin-after-m'); + $text->appendText('Create a feed to start sharing.'); + $card->appendChild($text); + + $button = $document->createElement('a'); + $button->setClassName('basic-button'); + $button->setAttribute('href', '/account/feeds/new'); + $button->appendText('Create Feed'); + $card->appendChild($button); + + return $card; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/IconButtonSnippet.php b/Frontend/src/Renderers/Snippet/IconButtonSnippet.php new file mode 100644 index 0000000..538ee82 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/IconButtonSnippet.php @@ -0,0 +1,48 @@ +iconSnippet = $iconSnippet; + } + + public function render(Document $document, string $icon, string $label, string $variant = null): Element + { + $className = 'light-button'; + + if ($variant !== null) { + $className = 'light-button ' . $variant; + } + + $button = $document->createElement('button'); + $button->setClassName($className); + + $buttonInner = $document->createElement('span'); + $buttonInner->setClassName('inner'); + $button->appendChild($buttonInner); + + $buttonInner->appendChild($this->iconSnippet->render($document, $icon, 'icon')); + + $buttonLabel = $document->createElement('span'); + $buttonLabel->setClassName('label'); + $buttonLabel->appendText($label); + $buttonInner->appendChild($buttonLabel); + + return $button; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/IconSnippet.php b/Frontend/src/Renderers/Snippet/IconSnippet.php new file mode 100644 index 0000000..40dc17b --- /dev/null +++ b/Frontend/src/Renderers/Snippet/IconSnippet.php @@ -0,0 +1,23 @@ +createElement('svg'); + $svgElement->setClassName($className); + + $useElement = $document->createElement('use'); + $useElement->setAttribute('xlink:href', '/icons/' . $iconName . '.svg#icon'); + $svgElement->appendChild($useElement); + + return $svgElement; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/PaginationButtonSnippet.php b/Frontend/src/Renderers/Snippet/PaginationButtonSnippet.php new file mode 100644 index 0000000..1625ab7 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/PaginationButtonSnippet.php @@ -0,0 +1,31 @@ +getPage() >= $posts->getPages()) { + return $template->createDocumentFragment(); + } + + $paginationButton = $template->createElement('button'); + $paginationButton->setClassName('pagination-button'); + $paginationButton->setAttribute('is', 'pagination-button'); + $paginationButton->setAttribute('loading-text', $this->getTranslator()->translate('Loading') . '...'); + $paginationButton->setAttribute('idle-text', $this->getTranslator()->translate('Load more')); + + return $paginationButton; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/PostAttachmentSnippet.php b/Frontend/src/Renderers/Snippet/PostAttachmentSnippet.php new file mode 100644 index 0000000..eea5593 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/PostAttachmentSnippet.php @@ -0,0 +1,50 @@ +iconSnippet = $iconSnippet; + } + + public function render(Dom\Document $document, array $attachment): Dom\Element + { + $attachmentElement = $document->createElement('div'); + $attachmentElement->setClassName('post-attachment'); + + $attachmentElement->appendChild($this->iconSnippet->render($document, 'attachment', 'icon')); + + $linkElement = $document->createElement('a'); + $linkElement->setClassName('name'); + $linkElement->setAttribute('href', $attachment['url']); + $linkElement->appendText($attachment['filename']); + + $attachmentElement->appendChild($linkElement); + + $downloadLinkElement = $document->createElement('a'); + $downloadLinkElement->setClassName('download'); + $downloadLinkElement->setAttribute('href', $attachment['url']); + $downloadLinkElement->setAttribute('download', ''); + $downloadLinkElement->appendText($this->getTranslator()->translate('Download')); + + $attachmentElement->appendChild($downloadLinkElement); + + return $attachmentElement; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/PostSnippet.php b/Frontend/src/Renderers/Snippet/PostSnippet.php new file mode 100644 index 0000000..6a6457f --- /dev/null +++ b/Frontend/src/Renderers/Snippet/PostSnippet.php @@ -0,0 +1,159 @@ +iconSnippet = $iconSnippet; + $this->uriBuilder = $uriBuilder; + $this->postAttachmentSnippet = $postAttachmentSnippet; + } + + public function render(Dom\Document $template, array $post, array $feed = []): Dom\Element + { + $cardClass = 'post-card'; + + if (isset($post['is_checked']) && $post['is_checked']) { + $cardClass = 'post-card -done'; + } + + $card = $template->createElement('article'); + $card->setClassName($cardClass); + + $header = $template->createElement('header'); + $header->setClassName('header'); + $card->appendChild($header); + + $time = $template->createElement('time-ago'); + $time->setClassName('time'); + $time->setAttribute('datetime', gmdate('c', $post['created'])); + $time->appendText(date('d.m.Y H:i:s', $post['created'])); + $header->appendChild($time); + + $body = $template->createElement('div'); + $body->setClassName('body'); + $card->appendChild($body); + + $title = $template->createElement('h2'); + $title->setClassName('title'); + + $body->appendChild($title); + + if ($post['type'] === 'task') { + $checkbox = $template->createElement('label'); + $checkbox->setClassName('task-checkbox checkbox'); + $title->appendChild($checkbox); + + $checkboxInput = $template->createElement('input'); + $checkboxInput->setClassName('checkbox'); + $checkboxInput->setAttribute('type', 'checkbox'); + + if (isset($post['is_checked']) && $post['is_checked']) { + $checkboxInput->setAttribute('checked', 'checked'); + } + + $checkbox->appendChild($checkboxInput); + $checkbox->appendChild($this->iconSnippet->render($template, 'task', 'icon')); + } + + $titleLink = $template->createElement('a'); + $titleLink->setAttribute('class', 'basic-link -no-bold'); + $titleLink->setAttribute('href', $this->uriBuilder->buildPostPageUri($post['id'])); + $titleLink->appendText($post['title']); + $title->appendChild($titleLink); + + $subtitle = $template->createElement('div'); + $subtitle->setClassName('subtitle'); + $body->appendChild($subtitle); + + $subtitle->appendText(new DisplayName($post['author'])); + $subtitle->appendText(' • '); + + $feedLink = $template->createElement('a'); + $feedLink->setClassName('basic-link'); + $feedLink->setAttribute('href', $this->uriBuilder->buildFeedPageUri($feed['id'])); + $feedLink->appendText($feed['name']); + $subtitle->appendChild($feedLink); + + if (!empty($post['rendered_body'])) { + $content = $template->createElement('div'); + $content->setClassName('content post-content'); + + $body->appendChild($content); + + $fragment = $template->createHTMLFragment($post['rendered_body']); + + $content->appendChild($fragment); + } + + if (isset($post['attachments'])) { + $attachmentsElement = $template->createElement('div'); + $attachmentsElement->setClassName('attachments'); + $card->appendChild($attachmentsElement); + + foreach ($post['attachments'] as $attachment) { + $attachmentsElement->appendChild( + $this->postAttachmentSnippet->render($template, $attachment) + ); + } + } + + $buttons = $template->createElement('div'); + $buttons->setClassName('buttons'); + $card->appendChild($buttons); + + if (isset($feed['access']['post']) && $feed['access']['post']) { + $deleteButtonData = [ + 'post_id' => $post['id'], + 'feed_id' => $post['feed']['id'] + ]; + + $deleteButton = $template->createElement('button'); + $deleteButton->setClassName('light-button'); + $deleteButton->setAttribute('is', 'ajax-button'); + $deleteButton->setAttribute('post-uri', '/action/posts/delete'); + $deleteButton->setAttribute('post-data', json_encode($deleteButtonData)); + + $deleteButtonInner = $template->createElement('span'); + $deleteButtonInner->setClassName('inner'); + $deleteButton->appendChild($deleteButtonInner); + + $deleteIcon = $this->iconSnippet->render($template, 'actions/delete', 'icon'); + $deleteButtonInner->appendChild($deleteIcon); + + $deleteButtonInner->appendText($this->getTranslator()->translate('Delete')); + + $buttons->appendChild($deleteButton); + } + + return $card; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/SearchTabNavSnippet.php b/Frontend/src/Renderers/Snippet/SearchTabNavSnippet.php new file mode 100644 index 0000000..11690f3 --- /dev/null +++ b/Frontend/src/Renderers/Snippet/SearchTabNavSnippet.php @@ -0,0 +1,44 @@ +tabNavSnippet = $tabNavSnippet; + $this->uriBuilder = $uriBuilder; + } + + public function render(Document $document, Tab $active, string $query): Element + { + return $this->tabNavSnippet->render( + $document, + $active, + new Everything($this->uriBuilder->buildSearchPageUri($query)), + new Posts($this->uriBuilder->buildPostsSearchPageUri($query)), + new Feeds($this->uriBuilder->buildFeedsSearchPageUri($query)) + ); + } + } +} diff --git a/Frontend/src/Renderers/Snippet/TabNavSnippet.php b/Frontend/src/Renderers/Snippet/TabNavSnippet.php new file mode 100644 index 0000000..676971b --- /dev/null +++ b/Frontend/src/Renderers/Snippet/TabNavSnippet.php @@ -0,0 +1,51 @@ +iconSnippet = $iconSnippet; + } + + public function render(Dom\Document $document, Tab $activeTab, TabNavItem ...$items): Dom\Element + { + $navElement = $document->createElement('nav'); + $navElement->setClassName('tab-nav'); + + foreach ($items as $item) { + $itemClass = 'item'; + + if ($item->getTab() == $activeTab) { + $itemClass = 'item -active'; + } + + $itemElement = $document->createElement('a'); + $itemElement->setClassName($itemClass); + $itemElement->setAttribute('href', $item->getUri()); + $navElement->appendChild($itemElement); + + $iconElement = $this->iconSnippet->render($document, $item->getIcon(), 'icon'); + $itemElement->appendChild($iconElement); + + $labelElement = $document->createElement('span'); + $labelElement->appendText($item->getLabel()); + $itemElement->appendChild($labelElement); + } + + return $navElement; + } + } +} diff --git a/Frontend/src/Renderers/Snippet/UserRolesOptionsSnippet.php b/Frontend/src/Renderers/Snippet/UserRolesOptionsSnippet.php new file mode 100644 index 0000000..8104e0b --- /dev/null +++ b/Frontend/src/Renderers/Snippet/UserRolesOptionsSnippet.php @@ -0,0 +1,42 @@ +createDocumentFragment(); + + $fragment->appendChild($this->renderOption($document, new DefaultUserRole, $selected)); + $fragment->appendChild($this->renderOption($document, new Moderator, $selected)); + $fragment->appendChild($this->renderOption($document, new Owner, $selected)); + + return $fragment; + } + + private function renderOption(Document $document, UserRole $role, string $selected = null): Element + { + $value = (string) $role; + $option = $document->createElement('option', $role->getLabel()); + + $option->setAttribute('value', $value); + + if ($value === $selected) { + $option->setAttribute('selected', ''); + } + + return $option; + } + } +} diff --git a/Frontend/src/Routers/ActionRouter.php b/Frontend/src/Routers/ActionRouter.php new file mode 100644 index 0000000..cdc8ff3 --- /dev/null +++ b/Frontend/src/Routers/ActionRouter.php @@ -0,0 +1,46 @@ +factory = $factory; + } + + public function route(RequestInterface $request): ControllerInterface + { + switch ($request->getUri()->getPath()) { + case '/action/register': + return $this->factory->createRegisterController(); + case '/action/login': + return $this->factory->createLoginController(); + case '/action/resend-verification': + return $this->factory->createResendVerificationController(); + case '/action/beta-request': + return $this->factory->createCreateBetaRequestController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\PostRequest; + } + } +} diff --git a/Frontend/src/Routers/FeedPageRouter.php b/Frontend/src/Routers/FeedPageRouter.php new file mode 100644 index 0000000..ce5523c --- /dev/null +++ b/Frontend/src/Routers/FeedPageRouter.php @@ -0,0 +1,101 @@ +factory = $factory; + $this->fetchFeedQuery = $fetchFeedQuery; + $this->lookupVanityQuery = $lookupVanityQuery; + $this->isLoggedInQuery = $isLoggedInQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + $parts = $request->getUri()->getExplodedPath(); + $count = count($parts); + + if ($parts[0] !== 'feed' || $count < 2) { + throw new RouterException; + } + + $feed = $this->fetchFeedQuery->execute($this->getFeedId($parts[1])); + + if ($feed === null) { + throw new RouterException; + } + + if ($count === 2) { + return $this->factory->createGetFeedPageController($feed); + } + + if ($count === 3 && $parts[2] === 'people') { + return $this->factory->createGetFeedPeoplePageController($feed); + } + + if (!$feed['access']['post']) { + throw new RouterException; + } + + if ($count === 3 && $parts[2] === 'note') { + return $this->factory->createGetCreatePostPageController($feed); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + + private function getFeedId(string $value) + { + $feedId = $this->lookupVanityQuery->execute($value); + + if ($feedId !== null) { + return $feedId; + } + + return $value; + } + } +} diff --git a/Frontend/src/Routers/FragmentRouter.php b/Frontend/src/Routers/FragmentRouter.php new file mode 100644 index 0000000..6d4253d --- /dev/null +++ b/Frontend/src/Routers/FragmentRouter.php @@ -0,0 +1,46 @@ +factory = $factory; + } + + public function route(RequestInterface $request): ControllerInterface + { + $parts = $request->getUri()->getExplodedPath(); + $count = count($parts); + + if (!isset($parts[0]) || $parts[0] !== 'fragment') { + throw new RouterException; + } + + if ($count === 3 && $parts[1] === 'feed-posts') { + return $this->factory->createGetFeedPostsFragmentController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Routers/NotFoundRouter.php b/Frontend/src/Routers/NotFoundRouter.php new file mode 100644 index 0000000..524686d --- /dev/null +++ b/Frontend/src/Routers/NotFoundRouter.php @@ -0,0 +1,34 @@ +factory = $factory; + } + + public function route(RequestInterface $request): ControllerInterface + { + return $this->factory->createStaticPageController('404', $request->getLanguage()); + } + + public function canHandle(RequestInterface $request): bool + { + return true; + } + } +} diff --git a/Frontend/src/Routers/PageRouter.php b/Frontend/src/Routers/PageRouter.php new file mode 100644 index 0000000..fe45506 --- /dev/null +++ b/Frontend/src/Routers/PageRouter.php @@ -0,0 +1,40 @@ +factory = $factory; + } + + public function route(RequestInterface $request): ControllerInterface + { + switch ($request->getUri()->getPath()) { + case '/account/verify': + return $this->factory->createVerifyAccountController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Routers/PostPageRouter.php b/Frontend/src/Routers/PostPageRouter.php new file mode 100644 index 0000000..e0e19c8 --- /dev/null +++ b/Frontend/src/Routers/PostPageRouter.php @@ -0,0 +1,55 @@ +factory = $factory; + $this->fetchPostQuery = $fetchPostQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + $parts = $request->getUri()->getExplodedPath(); + $count = count($parts); + + if ($count !== 2 || $parts[0] !== 'post') { + throw new RouterException; + } + + $post = $this->fetchPostQuery->execute($parts[1]); + + if ($post === null) { + throw new RouterException; + } + + return $this->factory->createGetPostPageController($post); + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Routers/StaticPageRouter.php b/Frontend/src/Routers/StaticPageRouter.php new file mode 100644 index 0000000..ad35464 --- /dev/null +++ b/Frontend/src/Routers/StaticPageRouter.php @@ -0,0 +1,50 @@ +factory = $factory; + $this->dataStoreReader = $dataStoreReader; + } + + public function route(RequestInterface $request): ControllerInterface + { + $path = $request->getUri()->getPath(); + + if (!$this->dataStoreReader->hasRoute($path)) { + throw new RouterException; + } + + $name = $this->dataStoreReader->getRoute($path); + + return $this->factory->createStaticPageController($name, $request->getLanguage()); + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Routers/UserActionRouter.php b/Frontend/src/Routers/UserActionRouter.php new file mode 100644 index 0000000..d29a01e --- /dev/null +++ b/Frontend/src/Routers/UserActionRouter.php @@ -0,0 +1,71 @@ +factory = $factory; + $this->isLoggedInQuery = $isLoggedInQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + if (!$this->isLoggedInQuery->execute()) { + throw new RouterException; + } + + switch ($request->getUri()->getPath()) { + case '/action/account/feeds/new': + return $this->factory->createNewFeedController(); + case '/action/logout': + return $this->factory->createLogoutController(); + case '/action/note/create': + return $this->factory->createCreateNoteController(); + case '/action/follow': + return $this->factory->createFollowController(); + case '/action/unfollow': + return $this->factory->createUnfollowController(); + case '/action/posts/delete': + return $this->factory->createDeletePostController(); + case '/action/feed/delete-user': + return $this->factory->createDeleteFeedUserController(); + case '/action/feed/update-user': + return $this->factory->createUpdateFeedUserRoleController(); + case '/action/feed/invite-user': + return $this->factory->createInviteFeedUserController(); + case '/action/feed/delete-invitation': + return $this->factory->createDeleteFeedInvitationController(); + case '/action/upload': + return $this->factory->createCreateUploadController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\PostRequest; + } + } +} diff --git a/Frontend/src/Routers/UserFragmentRouter.php b/Frontend/src/Routers/UserFragmentRouter.php new file mode 100644 index 0000000..f043fd6 --- /dev/null +++ b/Frontend/src/Routers/UserFragmentRouter.php @@ -0,0 +1,51 @@ +factory = $factory; + $this->isLoggedInQuery = $isLoggedInQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + if (!$this->isLoggedInQuery->execute()) { + throw new RouterException; + } + + switch ($request->getUri()->getPath()) { + case '/fragment/homepage-posts': + return $this->factory->createGetHomepagePostsFragmentController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Routers/UserPageRouter.php b/Frontend/src/Routers/UserPageRouter.php new file mode 100644 index 0000000..19f2926 --- /dev/null +++ b/Frontend/src/Routers/UserPageRouter.php @@ -0,0 +1,59 @@ +factory = $factory; + $this->isLoggedInQuery = $isLoggedInQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + if (!$this->isLoggedInQuery->execute()) { + throw new RouterException; + } + + switch ($request->getUri()->getPath()) { + case '/': + return $this->factory->createHomepageController(); + case '/feeds': + return $this->factory->createFeedsPageController(); + case '/search': + return $this->factory->createSearchPageController(new \Timetabio\Library\SearchTypes\All); + case '/search/feeds': + return $this->factory->createSearchPageController(new \Timetabio\Library\SearchTypes\Feed); + case '/search/posts': + return $this->factory->createSearchPageController(new \Timetabio\Library\SearchTypes\Post); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Frontend/src/Session/Session.php b/Frontend/src/Session/Session.php new file mode 100644 index 0000000..79ace7e --- /dev/null +++ b/Frontend/src/Session/Session.php @@ -0,0 +1,177 @@ +dataStoreReader = $dataStoreReader; + } + + public function getSessionId(): string + { + if ($this->sessionId === null) { + $this->sessionId = (string) new Token; + } + + return $this->sessionId; + } + + public function getExpires(): int + { + return $this->expires; + } + + public function getCrfsToken(): Token + { + $this->load(); + + if (!$this->data->has('crfsToken')) { + $this->data->set('crfsToken', new Token); + } + + return $this->data->get('crfsToken'); + } + + public function setUser(User $user) + { + $this->load(); + + $this->data->set('user', $user); + } + + public function removeUser() + { + $this->load(); + + $this->data->remove('user'); + } + + public function hasUser(): bool + { + $this->load(); + + return $this->data->has('user'); + } + + public function getUser(): User + { + $this->load(); + + return $this->data->get('user'); + } + + public function setAccessToken(string $accessToken) + { + $this->load(); + $this->data->set('accessToken', $accessToken); + } + + public function hasAccessToken(): bool + { + $this->load(); + + return $this->data->has('accessToken'); + } + + public function getAccessToken(): string + { + $this->load(); + + return $this->data->get('accessToken'); + } + + public function removeAccessToken() + { + $this->load(); + + $this->data->remove('accessToken'); + } + + public function getCookie(): Cookie + { + return new Cookie('session_id', $this->getSessionId(), '/', time() + $this->expires); + } + + public function getData(): MapInterface + { + $this->load(); + + return $this->data; + } + + public function loadRequest(RequestInterface $request) + { + if (!$request->hasCookie('session_id')) { + return; + } + + $sessionId = $request->getCookie('session_id'); + + if (!$this->dataStoreReader->hasSessionData($sessionId)) { + return; + } + + $this->sessionId = $sessionId; + } + + private function load() + { + if ($this->isLoaded) { + return; + } + + $this->data = $this->loadData(); + $this->isLoaded = true; + } + + private function loadData(): MapInterface + { + if ($this->sessionId === null) { + return new Map; + } + + if (!$this->dataStoreReader->hasSessionData($this->sessionId)) { + return new Map; + } + + return $this->dataStoreReader->getSessionData($this->sessionId); + } + } +} diff --git a/Frontend/src/TabNavItems/AbstractTabNavItem.php b/Frontend/src/TabNavItems/AbstractTabNavItem.php new file mode 100644 index 0000000..f327f0f --- /dev/null +++ b/Frontend/src/TabNavItems/AbstractTabNavItem.php @@ -0,0 +1,24 @@ +uri = $uri; + } + + public function getUri(): string + { + return $this->uri; + } + } +} diff --git a/Frontend/src/TabNavItems/FeedPage/People.php b/Frontend/src/TabNavItems/FeedPage/People.php new file mode 100644 index 0000000..f01fb8f --- /dev/null +++ b/Frontend/src/TabNavItems/FeedPage/People.php @@ -0,0 +1,27 @@ +hasCanonicalUri()) { + return; + } + + $canonicalTag = $template->createElement('link'); + $canonicalTag->setAttribute('rel', 'canonical'); + $canonicalTag->setAttribute('href', $model->getCanonicalUri()); + + $template->queryOne('//head')->appendChild($canonicalTag); + } + } +} diff --git a/Frontend/src/Transformations/CsrfTokenTransformation.php b/Frontend/src/Transformations/CsrfTokenTransformation.php new file mode 100644 index 0000000..0c828f1 --- /dev/null +++ b/Frontend/src/Transformations/CsrfTokenTransformation.php @@ -0,0 +1,19 @@ +queryOne('/html/head/meta[@name="csrf-token"]'); + $metaElement->setAttribute('content', $model->getCrfsToken()); + } + } +} diff --git a/Frontend/src/Transformations/TitleTransformation.php b/Frontend/src/Transformations/TitleTransformation.php new file mode 100644 index 0000000..697124f --- /dev/null +++ b/Frontend/src/Transformations/TitleTransformation.php @@ -0,0 +1,20 @@ +queryOne('/html/head/title') + ->appendText($model->getTitle() . ' · timetab.io'); + } + } +} diff --git a/Frontend/src/Transformations/TrackingTransformation.php b/Frontend/src/Transformations/TrackingTransformation.php new file mode 100644 index 0000000..1663834 --- /dev/null +++ b/Frontend/src/Transformations/TrackingTransformation.php @@ -0,0 +1,36 @@ +domBackend = $domBackend; + } + + public function apply(PageModel $model, Document $template) + { + if ($model->isTrackingDisabled()) { + return; + } + + $body = $template->queryOne('/html/body'); + $tracking = $this->domBackend->loadHtml('templates://content/tracking.html'); + + $body->appendChild($template->importDocument($tracking)); + } + } +} diff --git a/Frontend/src/Transformations/Transformer.php b/Frontend/src/Transformations/Transformer.php new file mode 100644 index 0000000..14842c2 --- /dev/null +++ b/Frontend/src/Transformations/Transformer.php @@ -0,0 +1,30 @@ +transformations = $transformations; + } + + public function apply(PageModel $model, Document $template) + { + foreach ($this->transformations as $transformation) { + $transformation->apply($model, $template); + } + } + } +} diff --git a/Frontend/src/Transformations/UserDropdownTransformation.php b/Frontend/src/Transformations/UserDropdownTransformation.php new file mode 100644 index 0000000..8c7b0f1 --- /dev/null +++ b/Frontend/src/Transformations/UserDropdownTransformation.php @@ -0,0 +1,36 @@ +hasUser()) { + return; + } + + $user = $model->getUser(); + $header = $template->queryOne('//header[@class="page-header"]/div'); + + if ($header === null) { + return; + } + + $link = $header->queryOne('//a[2]'); + $link->parentNode->removeChild($link); + + $username = $template->createElement('span'); + $username->appendText($user->getDisplayName()); + $username->setClassName('username right'); + + $header->appendChild($username); + } + } +} diff --git a/Frontend/src/ValueObjects/Feed.php b/Frontend/src/ValueObjects/Feed.php new file mode 100644 index 0000000..3b88fa0 --- /dev/null +++ b/Frontend/src/ValueObjects/Feed.php @@ -0,0 +1,83 @@ +feed = $feed; + } + + public function getId(): string + { + return $this->feed['id']; + } + + public function getName(): string + { + return $this->feed['name']; + } + + public function hasDescription(): bool + { + return !empty($this->feed['description']); + } + + public function getDescription(): string + { + return $this->feed['description']; + } + + public function hasPostAccess(): bool + { + return isset($this->feed['access']['post']) && $this->feed['access']['post']; + } + + public function hasUsersManageAccess(): bool + { + return isset($this->feed['access']['manage_users']) && $this->feed['access']['manage_users']; + } + + public function isUserInvited(): bool + { + return isset($this->feed['user']['invited']) && $this->feed['user']['invited']; + } + + public function isVerified(): bool + { + return isset($this->feed['is_verified']) && $this->feed['is_verified']; + } + + public function hasUserAdded(): bool + { + return isset($this->feed['user']['has_added']) && $this->feed['user']['has_added']; + } + + public function isPrivate(): bool + { + return isset($this->feed['is_private']) && $this->feed['is_private']; + } + + public function isPublic(): bool + { + return !$this->isPrivate(); + } + + /** + * @return array + * @deprecated To be used until all renderers/snippets support passing in a `Feed` instead of an array + */ + public function toArray(): array + { + return $this->feed; + } + } +} diff --git a/Frontend/src/ValueObjects/PaginatedResult.php b/Frontend/src/ValueObjects/PaginatedResult.php new file mode 100644 index 0000000..6e260ca --- /dev/null +++ b/Frontend/src/ValueObjects/PaginatedResult.php @@ -0,0 +1,87 @@ +results = $data['results']; + } + + if (isset($data['filter']['limit'])) { + $this->limit = $data['filter']['limit']; + } + + if (isset($data['filter']['page'])) { + $this->page = $data['filter']['page']; + } + + if (isset($data['meta']['total'])) { + $this->total = $data['meta']['total']; + } + + if (isset($data['meta']['pages'])) { + $this->pages = $data['meta']['pages']; + } + } + + public function getResults(): array + { + return $this->results; + } + + public function getLimit(): int + { + return $this->limit; + } + + public function getPage(): int + { + return $this->page; + } + + public function getPages(): int + { + return $this->pages; + } + + public function getTotal(): int + { + return $this->total; + } + + public function getIterator(): \Iterator + { + return new \ArrayIterator($this->results); + } + } +} diff --git a/Ink b/Ink new file mode 160000 index 0000000..aad45b0 --- /dev/null +++ b/Ink @@ -0,0 +1 @@ +Subproject commit aad45b01375b85790debc088cf85a126fef1bc35 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a871fcf --- /dev/null +++ b/LICENSE @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + diff --git a/Library/Rakefile b/Library/Rakefile new file mode 100644 index 0000000..8aef2f6 --- /dev/null +++ b/Library/Rakefile @@ -0,0 +1,18 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Library/bootstrap.php b/Library/bootstrap.php new file mode 100644 index 0000000..55bd38d --- /dev/null +++ b/Library/bootstrap.php @@ -0,0 +1,2 @@ +dataStoreReader = $dataStoreReader; + $this->uriHost = $uriHost; + } + + public function buildVerificationUri(string $token): string + { + return $this->uriHost . '/account/verify?' . http_build_query(['token' => $token]); + } + + public function buildFeedPageUri(string $feedId): string + { + return $this->uriHost . '/feed/' . $this->getFeedUriPart($feedId); + } + + public function buildNewPostPageUri(string $feedId): string + { + return $this->uriHost . '/feed/' . $this->getFeedUriPart($feedId) . '/note'; + } + + public function buildFeedPeoplePageUri(string $feedId): string + { + return $this->uriHost . '/feed/' . $this->getFeedUriPart($feedId) . '/people'; + } + + public function buildFeedOptionsPageUri(string $feedId): string + { + return $this->uriHost . '/feed/' . $this->getFeedUriPart($feedId) . '/options'; + } + + public function buildPostPageUri(string $postId): string + { + return $this->uriHost . '/post/' . $postId; + } + + public function buildSearchPageUri(string $query): string + { + return $this->uriHost . '/search?' . http_build_query(['q' => $query]); + } + + public function buildFeedsSearchPageUri(string $query): string + { + return $this->uriHost . '/search/feeds?' . http_build_query(['q' => $query]); + } + + public function buildPostsSearchPageUri(string $query): string + { + return $this->uriHost . '/search/posts?' . http_build_query(['q' => $query]); + } + + public function buildFileUri(string $publicId, string $filename): string + { + // Just a heads up dear developer from the future. + // DO NOT EVER EVEN think about changing `rawurlencode` to `urlencode`, + // it will break the amazon signature thingy for files with spaces in their name. + // This is due to the nature of `urlencode`, which encodes spaces as + instead of %20. + return $publicId . '/' . rawurlencode($filename); + } + + private function getFeedUriPart(string $feedId) + { + if ($this->dataStoreReader->hasFeedVanity($feedId)) { + return urlencode($this->dataStoreReader->getFeedVanity($feedId)); + } + + return $feedId; + } + } +} diff --git a/Library/src/DataObjects/FeedInvitation.php b/Library/src/DataObjects/FeedInvitation.php new file mode 100644 index 0000000..4efec43 --- /dev/null +++ b/Library/src/DataObjects/FeedInvitation.php @@ -0,0 +1,48 @@ +feedId = $feedId; + $this->userId = $userId; + $this->userRole = $userRole; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function getUserId(): string + { + return $this->userId; + } + + public function getUserRole(): UserRole + { + return $this->userRole; + } + } +} diff --git a/Library/src/DataObjects/StaticPage.php b/Library/src/DataObjects/StaticPage.php new file mode 100644 index 0000000..809bdcc --- /dev/null +++ b/Library/src/DataObjects/StaticPage.php @@ -0,0 +1,64 @@ +title = $title; + $this->content = $content; + $this->code = $code; + $this->showHeader = $showHeader; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getContent(): string + { + return $this->content; + } + + public function hasCode(): bool + { + return $this->code !== null; + } + + public function getCode(): StatusCodeInterface + { + return $this->code; + } + + public function getShowHeader(): bool + { + return $this->showHeader; + } + } +} diff --git a/Library/src/DataStore/AbstractDataStoreReader.php b/Library/src/DataStore/AbstractDataStoreReader.php new file mode 100644 index 0000000..722cb2b --- /dev/null +++ b/Library/src/DataStore/AbstractDataStoreReader.php @@ -0,0 +1,106 @@ +dataStore = $dataStore; + } + + protected function getDataStore(): DataStoreInterface + { + return $this->dataStore; + } + + public function hasPostBody(string $postId): bool + { + return $this->dataStore->has('post_body:' . $postId); + } + + public function getPostBody(string $postId): string + { + return $this->dataStore->get('post_body:' . $postId); + } + + public function hasPostPreview(string $postId): bool + { + return $this->dataStore->has('post_preview:' . $postId); + } + + public function getPostPreview(string $postId): string + { + return $this->dataStore->get('post_preview:' . $postId); + } + + public function hasFeedVanity(string $feedId): bool + { + return $this->dataStore->has('feed_vanity:' . $feedId); + } + + public function getFeedVanity(string $feedId): ?string + { + return $this->dataStore->get('feed_vanity:' . $feedId); + } + + public function hasVanity(string $vanity): bool + { + return $this->dataStore->has('vanity_feed:' . mb_strtolower($vanity)); + } + + public function getFeedByVanity(string $vanity): string + { + return $this->dataStore->get('vanity_feed:' . mb_strtolower($vanity)); + } + + public function hasFeedReadAccess(string $feedId, string $userId) + { + return $this->dataStore->hasInSet('feed_access_read:' . $feedId, $userId); + } + + public function hasFeedPostAccess(string $feedId, string $userId) + { + return $this->dataStore->hasInSet('feed_access_post:' . $feedId, $userId); + } + + public function hasFeedWriteAccess(string $feedId, string $userId) + { + return $this->dataStore->hasInSet('feed_access_write:' . $feedId, $userId); + } + + public function countFeedWriteAccess(string $feedId) + { + return $this->dataStore->sCard('feed_access_write:' . $feedId); + } + + public function isPrivateFeed(string $feedId) + { + return $this->dataStore->hasInSet('private_feeds', $feedId); + } + + public function hasFeed(string $feedId): bool + { + return $this->dataStore->hasInSet('feeds', $feedId); + } + + public function hasUserFeeds(string $userId): bool + { + return $this->getDataStore()->has('user_feeds:' . $userId); + } + + public function getUserFeeds(string $userId): array + { + return unserialize($this->getDataStore()->get('user_feeds:' . $userId)); + } + } +} diff --git a/Library/src/DataStore/AbstractDataStoreWriter.php b/Library/src/DataStore/AbstractDataStoreWriter.php new file mode 100644 index 0000000..8d43229 --- /dev/null +++ b/Library/src/DataStore/AbstractDataStoreWriter.php @@ -0,0 +1,83 @@ +dataStore = $dataStore; + } + + protected function getDataStore(): DataStoreInterface + { + return $this->dataStore; + } + + public function removeFeedAccess(string $feedId, string $userId): void + { + $this->dataStore->removeFromSet('feed_access_read:' . $feedId, $userId); + $this->dataStore->removeFromSet('feed_access_post:' . $feedId, $userId); + $this->dataStore->removeFromSet('feed_access_write:' . $feedId, $userId); + } + + public function setFeedAccess(string $feedId, string $userId, UserRole $role): void + { + $this->dataStore->addToSet('feed_access_read:' . $feedId, $userId); + + if ($role instanceof Moderator) { + $this->dataStore->addToSet('feed_access_post:' . $feedId, $userId); + } + + if ($role instanceof Owner) { + $this->dataStore->addToSet('feed_access_write:' . $feedId, $userId); + } + } + + public function addPrivateFeed(string $feedId): void + { + $this->dataStore->addToSet('private_feeds', $feedId); + } + + public function addFeed(string $feedId): void + { + $this->getDataStore()->addToSet('feeds', $feedId); + } + + public function queueTask(TaskInterface $task): void + { + $score = time() - $task->getPriority()->getValue(); + $value = serialize($task); + + if ($this->getDataStore()->hasInSet('task_queue', $value)) { + return; + } + + $this->getDataStore()->zAdd('task_queue', $score, $value); + } + + public function setVanity(string $feedId, string $vanity): void + { + $this->getDataStore()->set('vanity_feed:' . mb_strtolower($vanity), $feedId); + $this->getDataStore()->set('feed_vanity:' . $feedId, $vanity); + } + + public function setUserFeeds(string $userId, array $feeds): void + { + $this->getDataStore()->set('user_feeds:' . $userId, serialize($feeds)); + } + } +} diff --git a/Library/src/Factories/ApplicationFactory.php b/Library/src/Factories/ApplicationFactory.php new file mode 100644 index 0000000..ba75633 --- /dev/null +++ b/Library/src/Factories/ApplicationFactory.php @@ -0,0 +1,19 @@ +getMasterFactory()->createDataStoreReader(), + $this->getMasterFactory()->getConfiguration()->get('uriHost') + ); + } + } +} diff --git a/Library/src/Factories/IndexerFactory.php b/Library/src/Factories/IndexerFactory.php new file mode 100644 index 0000000..3b4f49f --- /dev/null +++ b/Library/src/Factories/IndexerFactory.php @@ -0,0 +1,32 @@ +getMasterFactory()->createElasticBackend() + ); + } + + public function createPostIndexer(): \Timetabio\Library\Indexers\PostIndexer + { + return new \Timetabio\Library\Indexers\PostIndexer( + $this->getMasterFactory()->createElasticBackend() + ); + } + + public function createUserIndexer(): \Timetabio\Library\Indexers\UserIndexer + { + return new \Timetabio\Library\Indexers\UserIndexer( + $this->getMasterFactory()->createElasticBackend() + ); + } + } +} diff --git a/Library/src/Factories/LocatorFactory.php b/Library/src/Factories/LocatorFactory.php new file mode 100644 index 0000000..b8441c6 --- /dev/null +++ b/Library/src/Factories/LocatorFactory.php @@ -0,0 +1,21 @@ +getMasterFactory()->createDocumentMapper() + ); + } + + public function createFeedMapper(): \Timetabio\Library\Mappers\FeedMapper + { + return new \Timetabio\Library\Mappers\FeedMapper( + $this->getMasterFactory()->createDocumentMapper() + ); + } + } +} diff --git a/Library/src/Factories/ServiceFactory.php b/Library/src/Factories/ServiceFactory.php new file mode 100644 index 0000000..21299c8 --- /dev/null +++ b/Library/src/Factories/ServiceFactory.php @@ -0,0 +1,18 @@ +getMasterFactory()->createPostgresBackend() + ); + } + } +} diff --git a/Library/src/Indexers/FeedIndexer.php b/Library/src/Indexers/FeedIndexer.php new file mode 100644 index 0000000..0d70b0b --- /dev/null +++ b/Library/src/Indexers/FeedIndexer.php @@ -0,0 +1,33 @@ +elasticBackend = $elasticBackend; + } + + public function indexDocument(string $id, array $document): void + { + $document['_feed_id'] = $id; + + $this->elasticBackend->indexDocument('feed', $id, $document); + } + + public function deleteDocument(string $id): void + { + $this->elasticBackend->deleteDocument('feed', $id); + } + } +} diff --git a/Library/src/Indexers/Indexer.php b/Library/src/Indexers/Indexer.php new file mode 100644 index 0000000..ca8973c --- /dev/null +++ b/Library/src/Indexers/Indexer.php @@ -0,0 +1,13 @@ +elasticBackend = $elasticBackend; + } + + public function indexDocument(string $id, array $document): void + { + $feedId = $document['feed']['id']; + $document['_feed_id'] = $feedId; + + $this->elasticBackend->indexDocument('post', $id, $document); + } + + public function deleteDocument(string $id): void + { + $this->elasticBackend->deleteDocument('post', $id); + } + } +} diff --git a/Library/src/Indexers/UserIndexer.php b/Library/src/Indexers/UserIndexer.php new file mode 100644 index 0000000..1b5f289 --- /dev/null +++ b/Library/src/Indexers/UserIndexer.php @@ -0,0 +1,31 @@ +elasticBackend = $elasticBackend; + } + + public function indexDocument(string $id, array $document): void + { + $this->elasticBackend->indexDocument('user', $id, $document); + } + + public function deleteDocument(string $id): void + { + $this->elasticBackend->deleteDocument('user', $id); + } + } +} diff --git a/Library/src/Locators/SearchTypeLocator.php b/Library/src/Locators/SearchTypeLocator.php new file mode 100644 index 0000000..c9b8270 --- /dev/null +++ b/Library/src/Locators/SearchTypeLocator.php @@ -0,0 +1,23 @@ +getTimestamp(); + } + + if (isset($document['updated'])) { + $document['updated'] = (new StringDateTime($document['updated']))->getTimestamp(); + } + + return $document; + } + + /** + * @deprecated + */ + public function listMap(array $documents): array + { + $result = []; + + foreach ($documents as $document) { + $result[] = $this->map($document); + } + + return $result; + } + } +} diff --git a/Library/src/Mappers/FeedMapper.php b/Library/src/Mappers/FeedMapper.php new file mode 100644 index 0000000..d34991e --- /dev/null +++ b/Library/src/Mappers/FeedMapper.php @@ -0,0 +1,51 @@ +documentMapper = $documentMapper; + } + + public function map(array $feed): array + { + $mapped = $this->documentMapper->map($feed); + + if (isset($mapped['has_added'])) { + $mapped['user']['has_added'] = $mapped['has_added']; + unset($mapped['has_added']); + } + + if (isset($mapped['owner_id'])) { + $mapped['owner']['id'] = $mapped['owner_id']; + unset($mapped['owner_id']); + } + + if (isset($mapped['owner_username'])) { + $mapped['owner']['username'] = $mapped['owner_username']; + unset($mapped['owner_username']); + } + + if (isset($mapped['owner_name'])) { + $mapped['owner']['name'] = $mapped['owner_name']; + } + + if (isset($mapped['vanity']) && $mapped['vanity'] === '') { + unset($mapped['vanity']); + } + + unset($mapped['owner_name']); + + return $mapped; + } + } +} diff --git a/Library/src/Mappers/PostMapper.php b/Library/src/Mappers/PostMapper.php new file mode 100644 index 0000000..d78e55a --- /dev/null +++ b/Library/src/Mappers/PostMapper.php @@ -0,0 +1,65 @@ +documentMapper = $documentMapper; + } + + public function map(array $post): array + { + $mapped = $this->documentMapper->map($post); + + if (isset($mapped['parsed_body'])) { + $mapped['parsed_body'] = json_decode($mapped['parsed_body'], true); + } + + if (isset($mapped['feed_id'])) { + $mapped['feed']['id'] = $mapped['feed_id']; + unset($mapped['feed_id']); + } + + if (isset($mapped['feed_name'])) { + $mapped['feed']['name'] = $mapped['feed_name']; + unset($mapped['feed_name']); + } + + if (isset($mapped['author_id'])) { + $mapped['author']['id'] = $mapped['author_id']; + unset($mapped['author_id']); + } + + if (isset($mapped['author_username'])) { + $mapped['author']['username'] = $mapped['author_username']; + unset($mapped['author_username']); + } + + if (isset($mapped['author_name'])) { + $mapped['author']['name'] = $mapped['author_name']; + unset($mapped['author_name']); + } + + if (isset($mapped['timestamp']) && $mapped['timestamp'] !== null) { + $mapped['timestamp'] = (new StringDateTime($mapped['timestamp']))->getTimestamp(); + } + + if (isset($mapped['user_id'])) { + unset($mapped['user_id']); + } + + return $mapped; + } + } +} diff --git a/Library/src/PostTypes/Event.php b/Library/src/PostTypes/Event.php new file mode 100644 index 0000000..465cd92 --- /dev/null +++ b/Library/src/PostTypes/Event.php @@ -0,0 +1,14 @@ +databaseBackend = $databaseBackend; + } + + public function getInvitations(string $feedId): array + { + return $this->databaseBackend->fetchAll( + 'SELECT invitation.*, users.name, users.username + FROM feed_invitations AS invitation + JOIN users ON invitation.user_id = users.id + WHERE invitation.feed_id = :feed_id + ORDER BY invitation.created DESC', + [ + 'feed_id' => $feedId + ] + ); + } + + public function getInvitation(string $feedId, string $userId) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM feed_invitations + WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + + public function hasInvitation(string $feedId, string $userId): bool + { + $result = $this->databaseBackend->fetch( + 'SELECT 1 FROM feed_invitations + WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + + return ($result !== null); + } + + public function createInvitation(FeedInvitation $invitation): array + { + return $this->databaseBackend->fetch( + 'INSERT INTO feed_invitations (feed_id, user_id, role) + VALUES (:feed_id, :user_id, :role) + RETURNING *', + [ + 'feed_id' => $invitation->getFeedId(), + 'user_id' => $invitation->getUserId(), + 'role' => (string) $invitation->getUserRole() + ] + ); + } + + public function deleteInvitation(string $feedId, string $userId) + { + $this->databaseBackend->execute( + 'DELETE FROM feed_invitations + WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + + public function updateInvitation(string $feedId, string $userId, UserRole $userRole) + { + $this->databaseBackend->execute( + 'UPDATE feed_invitations + SET role = :role + WHERE feed_id = :feed_id AND user_id = :user_id', + [ + 'role' => (string) $userRole, + 'feed_id' => $feedId, + 'user_id' => $userId + ] + ); + } + } +} diff --git a/Library/src/TaskPriorities/High.php b/Library/src/TaskPriorities/High.php new file mode 100644 index 0000000..386af0e --- /dev/null +++ b/Library/src/TaskPriorities/High.php @@ -0,0 +1,14 @@ +value = -random_int(0, $max); + } + + public function getValue(): int + { + return $this->value; + } + } +} diff --git a/Library/src/Tasks/BuildFeedTask.php b/Library/src/Tasks/BuildFeedTask.php new file mode 100644 index 0000000..199da0a --- /dev/null +++ b/Library/src/Tasks/BuildFeedTask.php @@ -0,0 +1,29 @@ +feedId = $feedId; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\Low; + } + } +} diff --git a/Library/src/Tasks/BuildFeedsTask.php b/Library/src/Tasks/BuildFeedsTask.php new file mode 100644 index 0000000..7f38b55 --- /dev/null +++ b/Library/src/Tasks/BuildFeedsTask.php @@ -0,0 +1,14 @@ +postId = $postId; + $this->priority = $priority; + } + + public function getPostId(): string + { + return $this->postId; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + if ($this->priority === null) { + return new \Timetabio\Library\TaskPriorities\Low; + } + + return $this->priority; + } + } +} diff --git a/Library/src/Tasks/BuildPostsTask.php b/Library/src/Tasks/BuildPostsTask.php new file mode 100644 index 0000000..eb8a90b --- /dev/null +++ b/Library/src/Tasks/BuildPostsTask.php @@ -0,0 +1,14 @@ +feedId = $feedId; + } + + public function getFeedId(): string + { + return $this->feedId; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\Normal; + } + } +} diff --git a/Library/src/Tasks/IndexFeedsTask.php b/Library/src/Tasks/IndexFeedsTask.php new file mode 100644 index 0000000..9139358 --- /dev/null +++ b/Library/src/Tasks/IndexFeedsTask.php @@ -0,0 +1,14 @@ +postId = $postId; + } + + public function getPostId(): string + { + return $this->postId; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\Normal; + } + } +} diff --git a/Library/src/Tasks/IndexPostsTask.php b/Library/src/Tasks/IndexPostsTask.php new file mode 100644 index 0000000..9a6ae3a --- /dev/null +++ b/Library/src/Tasks/IndexPostsTask.php @@ -0,0 +1,14 @@ + UpdateUserFeedsTask + */ + class IndexUserTask implements TaskInterface + { + /** + * @var string + */ + private $userId; + + public function __construct(string $userId) + { + $this->userId = $userId; + } + + public function getUserId(): string + { + return $this->userId; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\Normal; + } + } +} diff --git a/Library/src/Tasks/IndexUsersTask.php b/Library/src/Tasks/IndexUsersTask.php new file mode 100644 index 0000000..4f84e9b --- /dev/null +++ b/Library/src/Tasks/IndexUsersTask.php @@ -0,0 +1,14 @@ +invitation = $invitation; + } + + public function getInvitation(): FeedInvitation + { + return $this->invitation; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\High; + } + } +} diff --git a/Library/src/Tasks/SendVerificationEmailTask.php b/Library/src/Tasks/SendVerificationEmailTask.php new file mode 100644 index 0000000..a31efc4 --- /dev/null +++ b/Library/src/Tasks/SendVerificationEmailTask.php @@ -0,0 +1,43 @@ +person = $person; + $this->token = $token; + } + + public function getPerson(): EmailPerson + { + return $this->person; + } + + public function getToken(): Token + { + return $this->token; + } + + public function getPriority(): \Timetabio\Library\TaskPriorities\Priority + { + return new \Timetabio\Library\TaskPriorities\Normal; + } + } +} diff --git a/Library/src/Tasks/TaskInterface.php b/Library/src/Tasks/TaskInterface.php new file mode 100644 index 0000000..7715293 --- /dev/null +++ b/Library/src/Tasks/TaskInterface.php @@ -0,0 +1,11 @@ +translator = $translator; + } + + public function apply(PageModel $model, Document $template) + { + $translateElements = $template->query('//translate'); + + /** @var Element $element */ + foreach ($translateElements as $element) { + $key = $element->nodeValue; + $context = $element->getAttribute('context'); + + $message = $this->translator->translate($key, $context); + + $element->parentNode->replaceChild( + $template->createTextNode($message), + $element + ); + } + } + } +} diff --git a/Library/src/UserRoles/DefaultUserRole.php b/Library/src/UserRoles/DefaultUserRole.php new file mode 100644 index 0000000..ce492d7 --- /dev/null +++ b/Library/src/UserRoles/DefaultUserRole.php @@ -0,0 +1,19 @@ +value = $this->parse($user); + } + + private function parse(array $user): string + { + if (isset($user['name']) && $user['name'] !== '') { + return $user['name']; + } + + return '@' . $user['username']; + } + + public function __toString(): string + { + return $this->value; + } + } +} diff --git a/Locale/Rakefile b/Locale/Rakefile new file mode 100644 index 0000000..5ae9c86 --- /dev/null +++ b/Locale/Rakefile @@ -0,0 +1,30 @@ +require 'rake/clean' + +def locale_file(language) + file_name = "#{language}/LC_MESSAGES/messages.mo" + + file file_name => "#{language}/LC_MESSAGES/messages.po" do |t| + sh 'msgfmt', t.prerequisites[0], '-o', t.name + end + + CLEAN.include(file_name) + + file_name +end + +TARGETS = [ + locale_file('de_CH'), + locale_file('en_GB') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Locale/de_CH/LC_MESSAGES/messages.po b/Locale/de_CH/LC_MESSAGES/messages.po new file mode 100644 index 0000000..f03ec5f --- /dev/null +++ b/Locale/de_CH/LC_MESSAGES/messages.po @@ -0,0 +1,72 @@ +msgid "" +msgstr "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + +# API Errors + +msgid "invalid_login" +msgstr "Ungültige Anmeldedaten" + +msgid "invalid_email" +msgstr "Ungültige E-Mail" + +msgid "email_not_found" +msgstr "Kein Benutzer mit dieser E-Mail gefunden" + +msgid "invalid_feed_name" +msgstr "Bitte gib einen gültigen Namen für Deinen neuen Feed eint" + +msgid "user_not_verified" +msgstr "Dieses Konto ist noch nicht bestätigt" + + +# Messages + +msgid "Welcome" +msgstr "Willkommen" + +msgid "Sign in" +msgstr "Anmelden" + +msgid "Sign In" +msgstr "Anmelden" + +msgid "Register" +msgstr "Konto erstellen" + +msgid "Sign in to timetab.io" +msgstr "Bei timetab.io anmelden" + +msgid "Username or Email" +msgstr "Benutzername oder E-Mail" + +msgid "Password" +msgstr "Passwort" + +msgid "Don't have an account?" +msgstr "Noch kein Konto?" + +msgid "Create an Account" +msgstr "Konto erstellen" + +msgid "Username" +msgstr "Benutzername" + +msgid "Email" +msgstr "E-Mail" + +msgid "Already have an account?" +msgstr "Du hast schon ein Konto?" + +msgid "Create account" +msgstr "Konto erstellen" + +msgid "Not Found" +msgstr "Seite nicht gefunden" + +msgid "Create Feed" +msgstr "Feed erstellen" + +msgid "Create New Post" +msgstr "Neuen Post erstellen" diff --git a/Locale/en_GB/LC_MESSAGES/messages.po b/Locale/en_GB/LC_MESSAGES/messages.po new file mode 100644 index 0000000..3b30634 --- /dev/null +++ b/Locale/en_GB/LC_MESSAGES/messages.po @@ -0,0 +1,48 @@ +msgid "" +msgstr "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + +# API Errors + +msgid "invalid_login" +msgstr "Invalid credentials" + +msgid "invalid_email" +msgstr "Please enter a valid email address" + +msgid "invalid_username" +msgstr "Please enter a valid username (letters, numbers, -, _)" + +msgid "invalid_password" +msgstr "Your password must be at least 8 characters long" + +msgid "email_already_registered" +msgstr "This email is already registered" + +msgid "email_not_found" +msgstr "No user found with this email" + +msgid "invalid_feed_name" +msgstr "Please enter a name for your new feed" + +msgid "user_not_verified" +msgstr "Your account is not verified" + +msgid "empty_post_title" +msgstr "You have to enter a post title" + +msgid "email_already_added" +msgstr "This email has already been added" + +msgid "email_not_invited" +msgstr "This email has not been approved for the private beta" + +msgid "invitation_exists" +msgstr "This user is already invited" + +msgid "already_added" +msgstr "This user is already added" + +msgid "user_not_found" +msgstr "No user found with this username" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a469a11 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# code + +**TODO:** Installation, deployment, etc. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..27c291d --- /dev/null +++ b/Rakefile @@ -0,0 +1,25 @@ +DIRS = %w(API Framework Frontend Styles Application Worker Library Ink Locale Survey) + +task default: DIRS.map(&:downcase) + +DIRS.each do |dir| + desc "Build #{dir}" + task dir.downcase do + sh "cd #{dir} && rake" + end +end + +%w(clean deps test).each do |task_name| + desc "Run #{task_name} for all subdirectories" + + task task_name do + DIRS.each do |dir| + sh "cd #{dir} && rake #{task_name}" + end + end +end + +desc "Builds the RPM packages" +task :rpm do + sh './scripts/build-packages.sh' +end diff --git a/Showcase/css b/Showcase/css new file mode 120000 index 0000000..cccbdb9 --- /dev/null +++ b/Showcase/css @@ -0,0 +1 @@ +../Frontend/public/css \ No newline at end of file diff --git a/Showcase/favicon.ico b/Showcase/favicon.ico new file mode 120000 index 0000000..2b13c72 --- /dev/null +++ b/Showcase/favicon.ico @@ -0,0 +1 @@ +../Frontend/public/favicon.ico \ No newline at end of file diff --git a/Showcase/feed.html b/Showcase/feed.html new file mode 100644 index 0000000..7b35172 --- /dev/null +++ b/Showcase/feed.html @@ -0,0 +1,162 @@ + + + + + + + + + Showcase · timetab.io + + + + +
+ + + +
+ +
+

Mr. Ruby's Amazing Blog

+ + +
+ +
+ by Mr. Ruby +
+ +
+ +
+ +
+ +
+
+ Mr. Ruby + + + Mr. Ruby + +
+ +
+ 24 hours ago +
+ +
+ + Adventures +
+
+ + + +
+ +

+ + + + The Story of Hipster Ipsum +

+ +

+ Butcher hexagon jean shorts raclette, art party squid deep v meggings YOLO. Semiotics salvia + truffaut, locavore gluten-free before they sold out gentrify vexillologist. +

+ +

+ Before they sold out cliche fashion axe, vexillologist disrupt subway tile pork belly celiac + seitan pour-over hexagon ethical selfies live-edge tacos. 3 wolf moon cronut VHS, bicycle + rights food truck PBR&B slow-carb marfa gastropub before they sold out. +

+ +

+ ... +

+
+ + + + + +
+ +
+
+ + + + + + + diff --git a/Showcase/icons b/Showcase/icons new file mode 120000 index 0000000..23aaf7c --- /dev/null +++ b/Showcase/icons @@ -0,0 +1 @@ +../Styles/icons \ No newline at end of file diff --git a/Showcase/images b/Showcase/images new file mode 120000 index 0000000..5507899 --- /dev/null +++ b/Showcase/images @@ -0,0 +1 @@ +../Frontend/public/images \ No newline at end of file diff --git a/Showcase/index.html b/Showcase/index.html new file mode 100644 index 0000000..af04353 --- /dev/null +++ b/Showcase/index.html @@ -0,0 +1,112 @@ + + + + + + + + + Showcase · timetab.io + + +
+ The Showcase: A place to show the beauty of the components. +
+ + +
+ +
+ +
+ +
+ +
+

Title

+ + Link +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+ +
+ + The user "bash" does not exist + +
+ + + + + diff --git a/Showcase/js b/Showcase/js new file mode 120000 index 0000000..510173f --- /dev/null +++ b/Showcase/js @@ -0,0 +1 @@ +../Frontend/public/js \ No newline at end of file diff --git a/Showcase/new-post.html b/Showcase/new-post.html new file mode 100644 index 0000000..c8c8bcb --- /dev/null +++ b/Showcase/new-post.html @@ -0,0 +1,112 @@ + + + + + + + + + Showcase · timetab.io + + + + +
+ + + +
+ + + +
+
+ Mr. Ruby + + + Mr. Ruby + +
+ +
+ November 9, 2016 +
+
+ +
+ + + +
+ +
+
+ + + +
+ +
+
+ + + + + + + diff --git a/Styles/README.md b/Styles/README.md new file mode 100644 index 0000000..119380f --- /dev/null +++ b/Styles/README.md @@ -0,0 +1,3 @@ +# Styles + +Styles have been bootstrapped with [donut](https://github.com/bash/donut) as boilerplate by Ruben Schmidmeister, released under the [WTFPL](https://github.com/bash/donut/blob/master/LICENSE) diff --git a/Styles/Rakefile b/Styles/Rakefile new file mode 100644 index 0000000..4c84d39 --- /dev/null +++ b/Styles/Rakefile @@ -0,0 +1,24 @@ +require 'rake/clean' + +LESS_FILES = FileList['less/**/*.less'] + +TARGETS = %w(css/application.css) +CLEAN.concat(TARGETS) + +task :default => TARGETS + +desc 'Install dependencies' +task :deps do + sh 'npm', 'install', '-g' +end + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Build css bundle' +file 'css/application.css' => LESS_FILES do |t| + mkdir_p 'css' + sh "lessc --strict-math=on less/application.less | postcss -u autoprefixer -u cssnano -o #{t.name}" +end diff --git a/Styles/browserslist b/Styles/browserslist new file mode 100644 index 0000000..a4d1421 --- /dev/null +++ b/Styles/browserslist @@ -0,0 +1,7 @@ +# https://github.com/ai/browserslist +last 2 chrome versions +last 2 firefox versions +last 2 safari versions +last 2 ios_saf versions +last 2 opera versions +last 2 edge versions diff --git a/Styles/fonts/README.md b/Styles/fonts/README.md new file mode 100644 index 0000000..2ac0e90 --- /dev/null +++ b/Styles/fonts/README.md @@ -0,0 +1 @@ +Typeface: »Vollkorn« by Friedrich Althausen, [vollkorn-typeface.com](vollkorn-typeface.com) (Licensed under the [“SIL Open Font License” (OFL)](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL_web)) diff --git a/Styles/fonts/vollkorn-medium-webfont.woff b/Styles/fonts/vollkorn-medium-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..3eeddc071a3228f9c7a9518fe27bd4dd075bd740 GIT binary patch literal 58096 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

A2viz6Os}3Kt(xp4WphRmJ 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 literal 0 HcmV?d00001 diff --git a/Styles/icons/README.md b/Styles/icons/README.md new file mode 100644 index 0000000..33e4189 --- /dev/null +++ b/Styles/icons/README.md @@ -0,0 +1,2 @@ +Icons were created by [Cole Bemis](https://github.com/colebemis/feather) under MIT license. +SVG source code is modified (converted to symbols and unify size to 128px). diff --git a/Styles/icons/actions/delete.svg b/Styles/icons/actions/delete.svg new file mode 100644 index 0000000..3ef92eb --- /dev/null +++ b/Styles/icons/actions/delete.svg @@ -0,0 +1,12 @@ + + + Delete + + + + + + + + + \ No newline at end of file diff --git a/Styles/icons/actions/invite.svg b/Styles/icons/actions/invite.svg new file mode 100644 index 0000000..9796b47 --- /dev/null +++ b/Styles/icons/actions/invite.svg @@ -0,0 +1,10 @@ + + + Invite + + + + + + + \ No newline at end of file diff --git a/Styles/icons/actions/post.svg b/Styles/icons/actions/post.svg new file mode 100644 index 0000000..f63823d --- /dev/null +++ b/Styles/icons/actions/post.svg @@ -0,0 +1,13 @@ + + + Post + + + + + + + + + + diff --git a/Styles/icons/attachment.svg b/Styles/icons/attachment.svg new file mode 100644 index 0000000..48ba554 --- /dev/null +++ b/Styles/icons/attachment.svg @@ -0,0 +1,10 @@ + + + Attachment + + + + + + + diff --git a/Styles/icons/done.svg b/Styles/icons/done.svg new file mode 100644 index 0000000..81a9457 --- /dev/null +++ b/Styles/icons/done.svg @@ -0,0 +1,10 @@ + + + Done + + + + + + + \ No newline at end of file diff --git a/Styles/icons/event.svg b/Styles/icons/event.svg new file mode 100644 index 0000000..1ee1255 --- /dev/null +++ b/Styles/icons/event.svg @@ -0,0 +1,10 @@ + + + Event + + + + + + + diff --git a/Styles/icons/feed.svg b/Styles/icons/feed.svg new file mode 100644 index 0000000..04b75d2 --- /dev/null +++ b/Styles/icons/feed.svg @@ -0,0 +1,10 @@ + + + Feed + + + + + + + \ No newline at end of file diff --git a/Styles/icons/label.svg b/Styles/icons/label.svg new file mode 100644 index 0000000..052a09d --- /dev/null +++ b/Styles/icons/label.svg @@ -0,0 +1,10 @@ + + + Label + + + + + + + diff --git a/Styles/icons/note.svg b/Styles/icons/note.svg new file mode 100644 index 0000000..e06055c --- /dev/null +++ b/Styles/icons/note.svg @@ -0,0 +1,12 @@ + + + Attachment + + + + + + + + + diff --git a/Styles/icons/options.svg b/Styles/icons/options.svg new file mode 100644 index 0000000..a156837 --- /dev/null +++ b/Styles/icons/options.svg @@ -0,0 +1,10 @@ + + + Options + + + + + + + \ No newline at end of file diff --git a/Styles/icons/person.svg b/Styles/icons/person.svg new file mode 100644 index 0000000..9df9291 --- /dev/null +++ b/Styles/icons/person.svg @@ -0,0 +1,10 @@ + + + Person + + + + + + + \ No newline at end of file diff --git a/Styles/icons/private.svg b/Styles/icons/private.svg new file mode 100644 index 0000000..3190d64 --- /dev/null +++ b/Styles/icons/private.svg @@ -0,0 +1,10 @@ + + + Private + + + + + + + diff --git a/Styles/icons/public.svg b/Styles/icons/public.svg new file mode 100644 index 0000000..2d42bad --- /dev/null +++ b/Styles/icons/public.svg @@ -0,0 +1,10 @@ + + + Public + + + + + + + diff --git a/Styles/icons/search.svg b/Styles/icons/search.svg new file mode 100644 index 0000000..6fda460 --- /dev/null +++ b/Styles/icons/search.svg @@ -0,0 +1,10 @@ + + + Search + + + + + + + diff --git a/Styles/icons/settings.svg b/Styles/icons/settings.svg new file mode 100644 index 0000000..e986d52 --- /dev/null +++ b/Styles/icons/settings.svg @@ -0,0 +1,10 @@ + + + Settings + + + + + + + \ No newline at end of file diff --git a/Styles/icons/task.svg b/Styles/icons/task.svg new file mode 100644 index 0000000..413041e --- /dev/null +++ b/Styles/icons/task.svg @@ -0,0 +1,10 @@ + + + Task + + + + + + + diff --git a/Styles/icons/twitter.svg b/Styles/icons/twitter.svg new file mode 100644 index 0000000..a6a391c --- /dev/null +++ b/Styles/icons/twitter.svg @@ -0,0 +1,10 @@ + + + Twitter + + + + + + + \ No newline at end of file diff --git a/Styles/icons/verified.svg b/Styles/icons/verified.svg new file mode 100644 index 0000000..90ffd5e --- /dev/null +++ b/Styles/icons/verified.svg @@ -0,0 +1,11 @@ + + + Delete + + + + + + + + \ No newline at end of file diff --git a/Styles/less/_helpers.less b/Styles/less/_helpers.less new file mode 100644 index 0000000..9e08776 --- /dev/null +++ b/Styles/less/_helpers.less @@ -0,0 +1,36 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +// +// This file contains a set of helper classes. +// Helper classes can be applied to any element and we should keep the number +// of those classes as low as possible. +// +// Helper classes are all prefixed by an underscore (_) +// see: http://rscss.io/helpers.html +// + +._margin-before { + margin-top: @default-margin; +} + +._margin-after { + margin-bottom: @default-margin; +} + +._margin-after-l { + margin-bottom: @margin-l; +} + +._margin-after-m { + margin-bottom: @margin-m; +} + +._margin-after-s { + margin-bottom: @margin-s; +} + +._margin-after-xs { + margin-bottom: @margin-xs-v; +} diff --git a/Styles/less/_mixins.less b/Styles/less/_mixins.less new file mode 100644 index 0000000..5e18e05 --- /dev/null +++ b/Styles/less/_mixins.less @@ -0,0 +1,7 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.focus-ring(@color) { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px fade(@color, 70%); +} diff --git a/Styles/less/_variables.less b/Styles/less/_variables.less new file mode 100644 index 0000000..c3276e9 --- /dev/null +++ b/Styles/less/_variables.less @@ -0,0 +1,54 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +@custom-tags: form-field, form-error, file-drop, file-upload, paginated-view, paginated-list, toast-message; + +@brand-blue: #231E88; +@brand-green: #39965C; +@brand-red: #CF654F; +@brand-grey: #ededed; +@brand-light-grey: #D9D9D9; +@brand-dark-grey: #8C8C8C; +@brand-black: #191919; + +@default-text-color: @brand-black; +@body-background-color: #F9F9F9; + +// todo: make this a brand color +@default-divider-color: #F2F2F2; + +@header-width: 1200px; +@content-width: 576px; + +@margin-xl: 72px; +@margin-l: 48px; +@margin-m: 32px; +@margin-sm: 24px; +@margin-s: 16px; +@margin-xs-v: 12px; +@margin-xs-h: 8px; + +@font-size-l: 32px; +@font-size-m: 24px; +@font-size-sm: 18px; +@font-size-s: 16px; +@font-size-xs: 14px; + +@font-serif: 'Vollkorn', serif; +@font-sans: 'Open Sans', sans-serif; +@font-mono: 'Inconsolata', monospace; + +// TODO: remove (there is no "default" margin anymore) +@default-margin: @margin-sm; + +@default-input-width: 260px; + +@card-border-radius: 5px; +@card-max-width: 576px; +@card-box-shadow: 0 0 2px 0 rgba(0, 0, 0, .12);; + +@button-border-radius: 6px; + +@smaller-device-width-max: 480px; +@smaller-device-width-min: 481px; diff --git a/Styles/less/application.less b/Styles/less/application.less new file mode 100644 index 0000000..f41c359 --- /dev/null +++ b/Styles/less/application.less @@ -0,0 +1,16 @@ +@charset "UTF-8"; + +/*! + * (c) 2016 timetab.io + */ + +@import '_variables'; +@import '_mixins'; + +@import 'fonts/vollkorn'; + +@import 'core/all'; +@import 'elements/all'; +@import 'components/all'; + +@import '_helpers'; diff --git a/Styles/less/components/all.less b/Styles/less/components/all.less new file mode 100644 index 0000000..fbeff1e --- /dev/null +++ b/Styles/less/components/all.less @@ -0,0 +1,18 @@ +@import 'basic/all'; +@import 'floating/all'; +@import 'page/all'; +@import 'form/all'; +@import 'light/all'; +@import 'post/all'; +@import 'search/all'; +@import 'survey/all'; +@import 'feed/all'; +@import 'task/all'; +@import 'toast/all'; +@import 'user/all'; +@import 'flex-container'; +@import 'feed-list'; +@import 'logo-link'; +@import 'tab-nav'; +@import 'pagination-button'; +@import 'generic-card'; diff --git a/Styles/less/components/basic/all.less b/Styles/less/components/basic/all.less new file mode 100644 index 0000000..fff91ee --- /dev/null +++ b/Styles/less/components/basic/all.less @@ -0,0 +1,8 @@ +@import 'basic-button'; +@import 'basic-heading-a'; +@import 'basic-heading-b'; +@import 'basic-icon'; +@import 'basic-input'; +@import 'basic-link'; +@import 'basic-paragraph'; +@import 'basic-pre'; diff --git a/Styles/less/components/basic/basic-button.less b/Styles/less/components/basic/basic-button.less new file mode 100644 index 0000000..6864808 --- /dev/null +++ b/Styles/less/components/basic/basic-button.less @@ -0,0 +1,56 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-button { + display: inline-block; + background-color: @brand-green; + color: #fff; + border-radius: 6px; + padding: 10px 20px; + transition: background-color .125s, box-shadow .1s; + + &.-full-width { + width: 100%; + } + + &.-small { + font-size: 14px; + padding: 4px 12px; + } + + &:hover { + background-color: lighten(@brand-green, 10%); + } + + &:active { + background-color: darken(@brand-green, 10%); + } + + &:focus { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px fade(@brand-green, 70%); + } + + &[disabled] { + background-color: @brand-light-grey; + } + + &:not([disabled]) { + cursor: pointer; + } + + &.-mono { + background-color: @default-text-color; + } + + &.-mono:hover { + background-color: lighten(@default-text-color, 10%); + } + + &.-mono:active { + background-color: darken(@default-text-color, 10%); + } + + &.-mono:focus { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px fade(@default-text-color, 70%); + } +} diff --git a/Styles/less/components/basic/basic-heading-a.less b/Styles/less/components/basic/basic-heading-a.less new file mode 100644 index 0000000..a6d8422 --- /dev/null +++ b/Styles/less/components/basic/basic-heading-a.less @@ -0,0 +1,11 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-heading-a { + font-size: 24px; + + // TODO: why the fuck did I do this. I fucking hate me from the past + &.-margin-after { + margin-bottom: 16px; + } +} diff --git a/Styles/less/components/basic/basic-heading-b.less b/Styles/less/components/basic/basic-heading-b.less new file mode 100644 index 0000000..dad302b --- /dev/null +++ b/Styles/less/components/basic/basic-heading-b.less @@ -0,0 +1,6 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-heading-b { + font-size: 18px; +} diff --git a/Styles/less/components/basic/basic-icon.less b/Styles/less/components/basic/basic-icon.less new file mode 100644 index 0000000..31f7d84 --- /dev/null +++ b/Styles/less/components/basic/basic-icon.less @@ -0,0 +1,16 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-icon { + &.-in-text { + width: 1em; + height: 1em; + } + + &.-inline { + display: inline; + position: relative; + top: 0.17em; + margin-right: @margin-xs-h; + } +} diff --git a/Styles/less/components/basic/basic-input.less b/Styles/less/components/basic/basic-input.less new file mode 100644 index 0000000..abca6dd --- /dev/null +++ b/Styles/less/components/basic/basic-input.less @@ -0,0 +1,16 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-input { + background-color: #fff; + border: 2px solid @brand-light-grey; + border-radius: 6px; + padding: 8px 20px; + transition: border-color .125s; + width: @default-input-width; + max-width: 100%; + + &:focus { + border-color: @brand-dark-grey; + } +} diff --git a/Styles/less/components/basic/basic-link.less b/Styles/less/components/basic/basic-link.less new file mode 100644 index 0000000..c48a942 --- /dev/null +++ b/Styles/less/components/basic/basic-link.less @@ -0,0 +1,16 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-link { + cursor: pointer; + font-weight: bold; + + &.-no-bold { + font-weight: inherit; + } + + &:hover, + &:focus { + text-decoration: underline; + } +} diff --git a/Styles/less/components/basic/basic-paragraph.less b/Styles/less/components/basic/basic-paragraph.less new file mode 100644 index 0000000..e91db62 --- /dev/null +++ b/Styles/less/components/basic/basic-paragraph.less @@ -0,0 +1,6 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-paragraph { + margin-bottom: 16px; +} diff --git a/Styles/less/components/basic/basic-pre.less b/Styles/less/components/basic/basic-pre.less new file mode 100644 index 0000000..4dd23e5 --- /dev/null +++ b/Styles/less/components/basic/basic-pre.less @@ -0,0 +1,6 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.basic-pre { + font-family: 'Inconsolata', monospace; +} diff --git a/Styles/less/components/feed-list.less b/Styles/less/components/feed-list.less new file mode 100644 index 0000000..d4f9b0e --- /dev/null +++ b/Styles/less/components/feed-list.less @@ -0,0 +1,28 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.feed-list { + display: flex; + flex-direction: column; + background-color: #fff; + border-radius: @card-border-radius; + + > .item { + .basic-link(); + + display: flex; + align-items: center; + padding: @margin-s @margin-sm; + } + + > .item > .icon { + &:extend(.basic-icon, .basic-icon.-in-text); + + margin-right: @margin-xs-h; + } + + > .item:not(:last-child) { + border-bottom: 2px solid @default-divider-color; + } +} diff --git a/Styles/less/components/feed/all.less b/Styles/less/components/feed/all.less new file mode 100644 index 0000000..790cc7b --- /dev/null +++ b/Styles/less/components/feed/all.less @@ -0,0 +1,2 @@ +@import 'feed-header'; +@import 'feed-card'; diff --git a/Styles/less/components/feed/feed-card.less b/Styles/less/components/feed/feed-card.less new file mode 100644 index 0000000..3cd4cab --- /dev/null +++ b/Styles/less/components/feed/feed-card.less @@ -0,0 +1,28 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.feed-card { + background-color: #fff; + width: @card-max-width; + max-width: 100%; + padding: @margin-m; + border-radius: @card-border-radius; + box-shadow: @card-box-shadow; + overflow: hidden; + + > .name { + font-size: @font-size-m; + font-weight: bold; + } + + > .name > .icon { + width: @font-size-s; + height: @font-size-s; + margin-right: @margin-xs-v; + } + + > .description { + margin-top: @margin-xs-v; + } +} diff --git a/Styles/less/components/feed/feed-header.less b/Styles/less/components/feed/feed-header.less new file mode 100644 index 0000000..e3f81fd --- /dev/null +++ b/Styles/less/components/feed/feed-header.less @@ -0,0 +1,67 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.feed-header { + display: flex; + align-items: center; + flex-direction: column; + padding: @margin-xl @margin-l (@margin-xl - @margin-l) @margin-l; + + > .title { + margin-bottom: @margin-xs-v; + } + + > .title > .text { + font-size: @font-size-l; + font-weight: bold; + display: inline; + vertical-align: middle; + } + + > .title > .verified { + width: 25px; + height: 25px; + margin-left: @margin-xs-v; + margin-top: 3px; + color: @brand-green; + vertical-align: middle; + } + + > .description { + font-size: @font-size-sm; + font-weight: bold; + margin-bottom: @margin-s; + } + + @media (max-width: @smaller-device-width-max) { + @margin-between: (@margin-xs-v / 2); + + & { + flex-direction: row; + flex-wrap: wrap; + padding: (@margin-l - @margin-between) + (@margin-sm - @margin-between) + (@margin-sm - @margin-between) + (@margin-sm - @margin-between); + } + + > .title { + width: 100%; + } + + > .description { + margin-bottom: 0; + } + + > .title, + > .description, + > .button { + margin: @margin-between; + } + + > .button { + order: 1; + } + } +} diff --git a/Styles/less/components/flex-container.less b/Styles/less/components/flex-container.less new file mode 100644 index 0000000..f75a362 --- /dev/null +++ b/Styles/less/components/flex-container.less @@ -0,0 +1,14 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.flex-container { + display: flex; + + &.-column { + flex-direction: column; + } + + &.-center-items { + align-items: center; + } +} diff --git a/Styles/less/components/floating/all.less b/Styles/less/components/floating/all.less new file mode 100644 index 0000000..6608535 --- /dev/null +++ b/Styles/less/components/floating/all.less @@ -0,0 +1,2 @@ +@import 'floating-button'; +@import 'floating-buttons'; \ No newline at end of file diff --git a/Styles/less/components/floating/floating-button.less b/Styles/less/components/floating/floating-button.less new file mode 100644 index 0000000..a3b72ee --- /dev/null +++ b/Styles/less/components/floating/floating-button.less @@ -0,0 +1,61 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.floating-button { + + @padding-v: 20px; + @padding-h: 10px; + + display: flex; + align-items: center; + background-color: @brand-green; + color: #fff; + padding: @padding-h @padding-v; + cursor: pointer; + transition: transform .2s, background-color .125s; + + > .icon { + width: 1em; + height: 1em; + margin-right: @margin-xs-h; + } + + &:active { + background-color: darken(@brand-green, 10%); + } + + &:focus { + box-shadow: 0 0 0 1px #fff, 0 0 0 3px fade(@brand-green, 70%); + } + + @media (min-width: @smaller-device-width-min) { + & { + border-top-left-radius: @card-border-radius; + border-bottom-left-radius: @card-border-radius; + transform: translateX(100%) translateX((-2 * @padding-v - @font-size-s)); + } + + > .label { + opacity: 0; + transition: opacity .2s; + } + + &:hover > .label, + &:focus > .label { + opacity: 1; + } + + &:hover, + &:focus { + transform: translateX(0); + } + } + + @media (max-width: @smaller-device-width-max) { + & { + flex-grow: 1; + flex-basis: 0; + justify-content: center; + } + } +} diff --git a/Styles/less/components/floating/floating-buttons.less b/Styles/less/components/floating/floating-buttons.less new file mode 100644 index 0000000..4636727 --- /dev/null +++ b/Styles/less/components/floating/floating-buttons.less @@ -0,0 +1,27 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.floating-buttons { + @media (min-width: @smaller-device-width-min) { + & { + position: fixed; + right: 0; + top: 100px; + } + + > .floating-button:not(:first-child) { + margin-top: 8px; + } + } + + @media (max-width: @smaller-device-width-max) { + & { + display: flex; + align-items: center; + } + + > .floating-button:not(:first-child) { + border-left: 2px solid rgba(0, 0, 0, .1) + } + } +} diff --git a/Styles/less/components/form/all.less b/Styles/less/components/form/all.less new file mode 100644 index 0000000..6e3cc64 --- /dev/null +++ b/Styles/less/components/form/all.less @@ -0,0 +1,4 @@ +@import 'form-box'; +@import 'form-checkbox'; +@import 'form-error'; +@import 'form-field'; diff --git a/Styles/less/components/form/form-box.less b/Styles/less/components/form/form-box.less new file mode 100644 index 0000000..a9a4411 --- /dev/null +++ b/Styles/less/components/form/form-box.less @@ -0,0 +1,12 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.form-box { + background-color: #fff; + padding: @margin-m; + margin: auto; + width: (@default-input-width + @default-margin * 2); + max-width: 100%; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .12); + border-radius: @card-border-radius; +} diff --git a/Styles/less/components/form/form-checkbox.less b/Styles/less/components/form/form-checkbox.less new file mode 100644 index 0000000..243e16c --- /dev/null +++ b/Styles/less/components/form/form-checkbox.less @@ -0,0 +1,11 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.form-checkbox { + display: flex; + align-items: center; + + > .checkbox { + margin-right: 8px; + } +} diff --git a/Styles/less/components/form/form-error.less b/Styles/less/components/form/form-error.less new file mode 100644 index 0000000..d014ddf --- /dev/null +++ b/Styles/less/components/form/form-error.less @@ -0,0 +1,9 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.form-error { + color: @brand-red; + font-size: 14px; + font-weight: bold; + text-align: center; +} diff --git a/Styles/less/components/form/form-field.less b/Styles/less/components/form/form-field.less new file mode 100644 index 0000000..106fe17 --- /dev/null +++ b/Styles/less/components/form/form-field.less @@ -0,0 +1,26 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.form-field { + display: block; + + > .label { + font-size: 14px; + margin-bottom: 8px; + } + + &.-required > .label::after { + content: ' *'; + font-weight: bold; + } + + > .input, + > .label { + display: block; + } + + &.-margin-after { + margin-bottom: 24px; + } +} diff --git a/Styles/less/components/generic-card.less b/Styles/less/components/generic-card.less new file mode 100644 index 0000000..4caf381 --- /dev/null +++ b/Styles/less/components/generic-card.less @@ -0,0 +1,14 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.generic-card { + background-color: #fff; + border-radius: @card-border-radius; + box-shadow: @card-box-shadow; + padding: @margin-m; + + &.-center { + text-align: center; + } +} diff --git a/Styles/less/components/light/all.less b/Styles/less/components/light/all.less new file mode 100644 index 0000000..acccd16 --- /dev/null +++ b/Styles/less/components/light/all.less @@ -0,0 +1,3 @@ +@import 'light-button'; +@import 'light-select'; +@import 'light-pill'; diff --git a/Styles/less/components/light/light-button.less b/Styles/less/components/light/light-button.less new file mode 100644 index 0000000..47aee4e --- /dev/null +++ b/Styles/less/components/light/light-button.less @@ -0,0 +1,85 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.light-button-mixin() { + display: inline-block; + padding: 4px 12px; + border-radius: @button-border-radius; + font-size: @font-size-xs; + transition: color .125s, box-shadow .125s, opacity .125s; + max-width: 100%; +} + +.light-button-color(@color, @text-color) { + color: @text-color; + background-color: @color; + + &:hover { + background-color: lighten(@color, 5%); + } + + &:active { + background-color: darken(@color, 5%); + } + + &:focus { + .focus-ring(@color); + } +} + +.light-button-default-color() { + color: @brand-black; + background-color: @brand-grey; + + &:hover { + color: lighten(@brand-black, 5%); + } + + &:active { + color: darken(@brand-black, 5%); + } + + &:focus { + .focus-ring(@brand-light-grey); + } +} + +// TODO: rename +.light-button { + .light-button-mixin(); + .light-button-default-color(); + + > .inner { + display: inline-flex; + align-items: center; + } + + > .inner > .icon { + &:extend(.basic-icon, .basic-icon.-in-text); + margin-right: @margin-xs-h; + flex-shrink: 0; + } + + > .inner > .label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .inner > .label > .name { + font-weight: bold; + } + + &.-color { + .light-button-color(@brand-green, #fff); + } + + &[disabled] { + opacity: .2; + } + + &:not([disabled]) { + cursor: pointer; + } +} diff --git a/Styles/less/components/light/light-pill.less b/Styles/less/components/light/light-pill.less new file mode 100644 index 0000000..400ce2d --- /dev/null +++ b/Styles/less/components/light/light-pill.less @@ -0,0 +1,4 @@ +.light-pill { + .light-button-mixin(); + .light-button-default-color(); +} diff --git a/Styles/less/components/light/light-select.less b/Styles/less/components/light/light-select.less new file mode 100644 index 0000000..ef27bf8 --- /dev/null +++ b/Styles/less/components/light/light-select.less @@ -0,0 +1,19 @@ +.light-select { + appearance: none; + + .light-button-mixin(); + .light-button-default-color(); + + @select-triangle-size: 8px; + @select-padding: 12px; + + padding-right: (@select-padding * 1.5 + @select-triangle-size); + background-image: url(/images/triangle.svg); + background-repeat: no-repeat; + background-size: @select-triangle-size; + background-position: center right @select-padding; + + &[disabled] { + opacity: .2; + } +} diff --git a/Styles/less/components/logo-link.less b/Styles/less/components/logo-link.less new file mode 100644 index 0000000..746a5f7 --- /dev/null +++ b/Styles/less/components/logo-link.less @@ -0,0 +1,6 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.logo-link { + line-height: 0; +} diff --git a/Styles/less/components/page/all.less b/Styles/less/components/page/all.less new file mode 100644 index 0000000..82ba102 --- /dev/null +++ b/Styles/less/components/page/all.less @@ -0,0 +1,4 @@ +@import 'page-banner'; +@import 'page-footer'; +@import 'page-wrapper'; +@import 'page-header'; diff --git a/Styles/less/components/page/page-banner.less b/Styles/less/components/page/page-banner.less new file mode 100644 index 0000000..b35350e --- /dev/null +++ b/Styles/less/components/page/page-banner.less @@ -0,0 +1,11 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.page-banner { + background-color: @brand-green; + color: #fff; + font-size: 14px; + text-align: center; + padding: 12px; + font-weight: bold; +} diff --git a/Styles/less/components/page/page-footer.less b/Styles/less/components/page/page-footer.less new file mode 100644 index 0000000..6fb5d20 --- /dev/null +++ b/Styles/less/components/page/page-footer.less @@ -0,0 +1,64 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.page-footer { + display: flex; + align-items: center; + align-self: center; + width: (@card-max-width + @margin-l * 2); + padding-right: @margin-l; + padding-left: @margin-l; + margin: @margin-sm 0; + max-width: 100%; + + > .nav, + > .copyright { + flex-basis: 0; + flex-grow: 1; + } + + > .nav > .item:not(:last-child) { + margin-right: @margin-xs-h; + } + + > .copyright { + text-align: right; + } + + > .logo { + text-align: center; + line-height: 0; + } + + > .logo > .image { + width: 40px; + height: 40px; + } + + @media (max-width: @smaller-device-width-max) { + & { + width: (@card-max-width + @margin-m * 2); + padding-right: @margin-m; + padding-left: @margin-m; + flex-wrap: wrap; + } + + > .logo { + display: none; + } + + > .nav, + > .copyright { + flex-basis: auto; + } + + > .copyright { + flex-grow: 0; + } + + > .nav { + text-align: left; + } + } +} diff --git a/Styles/less/components/page/page-header.less b/Styles/less/components/page/page-header.less new file mode 100644 index 0000000..c055355 --- /dev/null +++ b/Styles/less/components/page/page-header.less @@ -0,0 +1,72 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.page-header { + background-color: #fff; + box-shadow: 0 1px 2px 0 rgba(217, 217, 217, .5); + + > .content { + display: flex; + align-items: center; + padding: @margin-s; + width: (@header-width + 2 * @margin-s); + max-width: 100%; + margin: auto; + } + + > .content > .logo, + > .content > .right { + min-width: 200px; + } + + > .content > .logo { + flex-shrink: 0; + } + + > .content > .right { + text-align: right; + } + + > .content > .search { + flex-grow: 1; + display: flex; + justify-content: center; + padding: 0 @margin-sm; + } + + > .content > .search > .form { + flex-grow: 1; + } + + > .content > .username { + text-align: right; + font-weight: 600; + font-size: @font-size-xs; + } + + @media (max-width: 785px) { + > .content > .logo, + > .content > .right { + min-width: 0; + } + } + + @media (max-width: 518px) { + > .content { + flex-wrap: wrap; + } + + > .content > .logo { + flex-grow: 1; + } + + > .content > .search { + order: 1; + width: 100%; + padding: 0; + margin-top: @margin-s; + } + + } +} diff --git a/Styles/less/components/page/page-wrapper.less b/Styles/less/components/page/page-wrapper.less new file mode 100644 index 0000000..dcc7ce1 --- /dev/null +++ b/Styles/less/components/page/page-wrapper.less @@ -0,0 +1,25 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.page-wrapper { + width: @content-width; + max-width: 100%; + margin: auto; + + &.-padding { + width: (@content-width + 2 * @margin-l); + padding: @margin-l; + } + + &.-padding.-no-padding-top { + padding-top: 0; + } + + @media (max-width: @smaller-device-width-max) { + &.-padding { + width: (@content-width + 2 * @margin-sm); + padding: @margin-m @margin-sm; + } + } +} diff --git a/Styles/less/components/pagination-button.less b/Styles/less/components/pagination-button.less new file mode 100644 index 0000000..6c7d508 --- /dev/null +++ b/Styles/less/components/pagination-button.less @@ -0,0 +1,25 @@ +.pagination-button { + display: block; + width: 100%; + padding: 10px 20px; + border: 1px solid @brand-light-grey; + border-radius: 6px; + margin-top: @margin-l; + color: @brand-dark-grey; + cursor: pointer; + transition: background-color .125s; + + &:hover { + background-color: @default-divider-color; + } + + &:focus { + .focus-ring(@brand-light-grey); + } + + @media (max-width: @smaller-device-width-max) { + & { + margin-top: @margin-m; + } + } +} diff --git a/Styles/less/components/post/all.less b/Styles/less/components/post/all.less new file mode 100644 index 0000000..956793c --- /dev/null +++ b/Styles/less/components/post/all.less @@ -0,0 +1,5 @@ +@import 'post-attachment'; +@import 'post-card'; +@import 'post-card-outside-text'; +@import 'post-content'; +@import 'post-list'; diff --git a/Styles/less/components/post/post-attachment.less b/Styles/less/components/post/post-attachment.less new file mode 100644 index 0000000..2678ced --- /dev/null +++ b/Styles/less/components/post/post-attachment.less @@ -0,0 +1,84 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.post-attachment { + display: flex; + align-items: center; + padding: @margin-s @margin-m; + border-bottom: 2px solid @default-divider-color; + font-size: @font-size-xs; + + > .name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + } + + > .name:hover, + > .name:focus, + > .download:focus, + > .download:hover { + text-decoration: underline; + } + + > .name, + > .icon { + margin-right: @margin-xs-h; + } + + > .icon { + &:extend(.basic-icon); + &:extend(.basic-icon.-in-text); + flex-shrink: 0; + } + + > .download { + color: @brand-dark-grey; + } + + > .progress { + margin-left: @margin-xs-v; + flex-grow: 1; + display: flex; + justify-content: flex-end; + align-items: center; + } + + > .progress > .bar { + border-radius: 8px; + height: 6px; + width: 170px; + background-color: @default-divider-color; + background-image: linear-gradient(@brand-light-grey, @brand-light-grey); + background-size: 0 100%; + background-repeat: no-repeat; + transition: background-size .125s; + } + + > .progress > .done { + color: @brand-green; + width: 1em; + height: 1em; + margin-left: @margin-xs-v; + opacity: 0; + transition: opacity .2s; + } + + &.-uploaded > .progress > .done { + opacity: 1; + } + + @media (max-width: @smaller-device-width-max) { + & { + flex-wrap: wrap; + } + + > .progress { + width: 100%; + justify-content: flex-start; + margin-top: 4px; + margin-left: (@font-size-xs + @margin-xs-h); + } + } +} diff --git a/Styles/less/components/post/post-card-outside-text.less b/Styles/less/components/post/post-card-outside-text.less new file mode 100644 index 0000000..35a39bd --- /dev/null +++ b/Styles/less/components/post/post-card-outside-text.less @@ -0,0 +1,10 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.post-card-outside-text { + color: @brand-dark-grey; + margin-top: @margin-s; + margin-right: @margin-s; + margin-left: @margin-s; + font-size: @font-size-xs; +} diff --git a/Styles/less/components/post/post-card.less b/Styles/less/components/post/post-card.less new file mode 100644 index 0000000..b518920 --- /dev/null +++ b/Styles/less/components/post/post-card.less @@ -0,0 +1,92 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.post-card { + background-color: #fff; + width: @card-max-width; + max-width: 100%; + padding-top: @margin-m; + border-radius: @card-border-radius; + box-shadow: @card-box-shadow; + overflow: hidden; + transition: box-shadow .125s; + + &.-done { + color: @brand-light-grey; + } + + &.-drag-over { + box-shadow: 0 1px 5px rgba(0, 0, 0, .2); + } + + > .header, + > .body, + > .buttons { + padding-left: @margin-m; + padding-right: @margin-m; + } + + > .body > .textarea { + resize: none; + width: 100%; + } + + > .header, + > .body { + margin-bottom: @margin-m; + } + + > .attachments, + > .buttons { + margin-bottom: @margin-sm; + } + + > .header { + display: flex; + align-items: center; + font-size: @font-size-xs; + } + + > .header > .time { + text-align: center; + margin-right: @margin-s; + } + + > .header > .label { + display: flex; + align-items: center; + justify-content: flex-end; + } + + > .header > .label > .icon { + &:extend(.basic-icon, .basic-icon.-in-text); + margin-right: @margin-xs-h; + } + + > .body > .title { + font-size: 28px; + font-family: @font-serif; + font-weight: 500; + margin-bottom: @margin-xs-v; + display: flex; + align-items: center; + width: 100%; + } + + > .body > .title > .checkbox { + margin-right: @margin-xs-v; + } + + > .body > .subtitle { + margin-bottom: @margin-m; + font-size: @font-size-xs; + } + + > .body > .paragraph:not(:last-child) { + margin-bottom: @margin-xs-v; + } + + > .attachments > .post-attachment:first-child { + border-top: 2px solid @default-divider-color; + } +} diff --git a/Styles/less/components/post/post-content.less b/Styles/less/components/post/post-content.less new file mode 100644 index 0000000..6895a62 --- /dev/null +++ b/Styles/less/components/post/post-content.less @@ -0,0 +1,58 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.post-content { + // TODO: is this a good idea? + word-break: break-all; + + > h1, h2, h3, p, blockquote, pre, ul { + margin-bottom: @margin-s; + } + + > h1 { + font-size: 24px; + } + + h2 { + font-size: 18px; + } + + h3 { + font-weight: bold; + } + + > pre { + background-color: @default-divider-color; + padding: @margin-s; + font-family: @font-mono; + white-space: pre; + overflow: auto; + } + + > blockquote { + border-left: 3px solid @default-divider-color; + padding-left: @margin-s; + } + + > ul > li::before { + content: '\2022'; + margin-right: @margin-xs-h; + } + + a { + &:extend(.basic-link); + } + + a:focus, + a:hover { + text-decoration: underline; + } + + b { + font-weight: bold; + } + + i { + font-style: italic; + } +} diff --git a/Styles/less/components/post/post-list.less b/Styles/less/components/post/post-list.less new file mode 100644 index 0000000..858712f --- /dev/null +++ b/Styles/less/components/post/post-list.less @@ -0,0 +1,31 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +// TODO: rename to record-list or idk what, but not fucking post-list +.post-list { + // The approach with :last-child did not work out well in combination + // with chrome and loading dynamically + + .post-card:not(:first-child), + .feed-card:not(:first-child) { + margin-top: @margin-l; + } + + &.-smaller-margin .post-card:not(:first-child), + &.-smaller-margin .feed-card:not(:first-child) { + margin-top: @margin-s; + } + + @media (max-width: @smaller-device-width-max) { + > .post-card:not(:first-child), + > .feed-card:not(:first-child) { + margin-top: @margin-m; + } + + &.-smaller-margin .post-card:not(:first-child), + &.-smaller-margin .feed-card:not(:first-child) { + margin-top: @margin-s; + } + } +} diff --git a/Styles/less/components/search/all.less b/Styles/less/components/search/all.less new file mode 100644 index 0000000..9cb6cec --- /dev/null +++ b/Styles/less/components/search/all.less @@ -0,0 +1 @@ +@import 'search-form'; diff --git a/Styles/less/components/search/search-form.less b/Styles/less/components/search/search-form.less new file mode 100644 index 0000000..dff80fe --- /dev/null +++ b/Styles/less/components/search/search-form.less @@ -0,0 +1,42 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.search-form { + display: flex; + width: 100%; + max-width: 470px; + + > .input, + > .button { + background-color: @brand-grey; + } + + > .input { + flex-grow: 1; + width: 0; + align-items: stretch; + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 0; // ios safari + border-top-right-radius: 0; // ios safari + padding: 6px 20px; + } + + > .input::placeholder { + color: rgba(0, 0, 0, .5); + } + + > .button { + cursor: pointer; + padding: 9px 20px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + + > .button > .icon { + display: block; + width: 1em; + height: 1em; + } +} diff --git a/Styles/less/components/survey/all.less b/Styles/less/components/survey/all.less new file mode 100644 index 0000000..b55b304 --- /dev/null +++ b/Styles/less/components/survey/all.less @@ -0,0 +1 @@ +@import 'survey-question-card'; diff --git a/Styles/less/components/survey/survey-question-card.less b/Styles/less/components/survey/survey-question-card.less new file mode 100644 index 0000000..486eec8 --- /dev/null +++ b/Styles/less/components/survey/survey-question-card.less @@ -0,0 +1,33 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.survey-question-card { + background-color: #fff; + width: @card-max-width; + max-width: 100%; + padding: @margin-m; + border-radius: @card-border-radius; + box-shadow: @card-box-shadow; + margin-bottom: @margin-l; + font-family: @font-mono; + + > .title { + font-size: @font-size-m; + margin-bottom: @margin-sm; + line-height: 1.3; + } + + > .choice { + display: flex; + align-items: center; + } + + > .choice:not(:last-child) { + margin-bottom: @margin-xs-h; + } + + > .choice > .input { + margin-right: @margin-xs-h; + } +} diff --git a/Styles/less/components/tab-nav.less b/Styles/less/components/tab-nav.less new file mode 100644 index 0000000..bf1f562 --- /dev/null +++ b/Styles/less/components/tab-nav.less @@ -0,0 +1,54 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.tab-nav { + display: flex; + flex-wrap: wrap; + justify-content: center; + padding: 0 (@margin-l - @margin-m / 2); + margin: (@margin-xs-v / -2) 0; + font-size: 18px; + + &.-margin { + margin-top: (@margin-l - @margin-xs-v / 2); + margin-bottom: (@margin-l - @margin-xs-v / 2); + } + + > .item { + display: flex; + align-items: center; + cursor: pointer; + transition: opacity .125s; + margin: (@margin-xs-v / 2) (@margin-m / 2); + } + + > .item.-active { + color: @brand-green; + } + + > .item:focus, + > .item:hover { + opacity: .8; + } + + > .item > .icon { + width: 1em; + height: 1em; + margin-right: @margin-xs-h; + } + + @media (max-width: @smaller-device-width-max) { + & { + padding: 0 (@margin-sm - @margin-sm / 2); + } + + &.-responsive { + justify-content: flex-start; + } + + > .item { + margin: (@margin-xs-v / 2) (@margin-sm / 2); + } + } +} diff --git a/Styles/less/components/task/all.less b/Styles/less/components/task/all.less new file mode 100644 index 0000000..e47ce17 --- /dev/null +++ b/Styles/less/components/task/all.less @@ -0,0 +1 @@ +@import 'task-checkbox'; diff --git a/Styles/less/components/task/task-checkbox.less b/Styles/less/components/task/task-checkbox.less new file mode 100644 index 0000000..cd5dcd6 --- /dev/null +++ b/Styles/less/components/task/task-checkbox.less @@ -0,0 +1,33 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ +.task-checkbox { + display: inline-block; + height: 28px; + width: 28px; + position: relative; + + > .checkbox { + opacity: 0; + position: absolute; + top: 0; + left: 0; + } + + > .icon { + width: 100%; + height: 100%; + color: @brand-light-grey; + transition: color .125s; + cursor: pointer; + border-radius: 3px; + } + + > .checkbox:checked + .icon { + color: @brand-black; + } + + > .checkbox:focus + .icon { + .focus-ring(@brand-light-grey); + } +} diff --git a/Styles/less/components/toast/all.less b/Styles/less/components/toast/all.less new file mode 100644 index 0000000..d982e9f --- /dev/null +++ b/Styles/less/components/toast/all.less @@ -0,0 +1,2 @@ +@import 'toast-container'; +@import 'toast-message'; diff --git a/Styles/less/components/toast/toast-container.less b/Styles/less/components/toast/toast-container.less new file mode 100644 index 0000000..de5493a --- /dev/null +++ b/Styles/less/components/toast/toast-container.less @@ -0,0 +1,6 @@ +.toast-container { + position: fixed; + bottom: 0; + width: 100%; + padding: 0 @margin-m; +} diff --git a/Styles/less/components/toast/toast-message.less b/Styles/less/components/toast/toast-message.less new file mode 100644 index 0000000..bce1d51 --- /dev/null +++ b/Styles/less/components/toast/toast-message.less @@ -0,0 +1,39 @@ +@keyframes toast-message-appear { + from { + transform: translateY(50%); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes toast-message-disappear { + from { + transform: translateY(0%); + opacity: 1; + } + + to { + transform: translateY(50%); + opacity: 0; + } +} + +.toast-message { + color: #fff; + background-color: rgba(red(@brand-black), green(@brand-black), blue(@brand-black), .98); + padding: 14px 32px; + border-radius: @card-border-radius; + width: 100%; + max-width: @card-max-width; + margin: 0 auto @margin-sm auto; + + animation: toast-message-appear ease-in .3s; + + &.-disappear { + animation: toast-message-disappear ease-out .1s; + } +} diff --git a/Styles/less/components/user/all.less b/Styles/less/components/user/all.less new file mode 100644 index 0000000..440e84c --- /dev/null +++ b/Styles/less/components/user/all.less @@ -0,0 +1,2 @@ +@import 'user-list-item'; +@import 'user-list'; diff --git a/Styles/less/components/user/user-list-item.less b/Styles/less/components/user/user-list-item.less new file mode 100644 index 0000000..fcc845e --- /dev/null +++ b/Styles/less/components/user/user-list-item.less @@ -0,0 +1,33 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.user-list-item { + display: flex; + align-items: center; + border-radius: @card-border-radius; + background-color: #fff; + padding: @margin-s @margin-sm; + font-weight: 600; + + > .name { + flex-grow: 1; + margin-right: @margin-xs-v; + } + + > .role { + margin-right: @margin-xs-h; + flex-shrink: 0; + } + + @media (max-width: @smaller-device-width-max) { + & { + flex-wrap: wrap; + } + + > .name { + width: 100%; + margin-bottom: @margin-xs-v; + } + } +} diff --git a/Styles/less/components/user/user-list.less b/Styles/less/components/user/user-list.less new file mode 100644 index 0000000..ba87c70 --- /dev/null +++ b/Styles/less/components/user/user-list.less @@ -0,0 +1,16 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +.user-list { + border-radius: @card-border-radius; + overflow: hidden; + + > .user-list-item { + border-radius: 0; + } + + > .user-list-item:not(:last-child) { + border-bottom: 2px solid @default-divider-color; + } +} diff --git a/Styles/less/core/all.less b/Styles/less/core/all.less new file mode 100644 index 0000000..f232215 --- /dev/null +++ b/Styles/less/core/all.less @@ -0,0 +1,4 @@ +@import 'normalize'; +@import 'reset'; +@import 'typography'; +@import 'basics'; diff --git a/Styles/less/core/basics.less b/Styles/less/core/basics.less new file mode 100644 index 0000000..d6a33c1 --- /dev/null +++ b/Styles/less/core/basics.less @@ -0,0 +1,19 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +img { + max-width: 100%; +} + +body { + background-color: @body-background-color; + color: @default-text-color; + display: flex; + flex-direction: column; + min-height: 100vh; + + > main { + flex-grow: 1; + } +} diff --git a/Styles/less/core/normalize.less b/Styles/less/core/normalize.less new file mode 100644 index 0000000..a37206a --- /dev/null +++ b/Styles/less/core/normalize.less @@ -0,0 +1,68 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +html { + // Fix IOS Safari's behaviour in landscape mode + -webkit-text-size-adjust: 100%; +} + +[hidden] { + // make sure all browsers hide elements with [hidden] + display: none !important; +} + +fieldset, +img { + // in webkit browsers this defaults to -webkit-min-content for fieldsets + // ... and for some reason the safari technology preview needs this for images + min-width: 0; +} + +input { + min-width: 0; +} + +input:not([type=radio]):not([type=checkbox]) { + -webkit-appearance: none; + -moz-appearance: none; +} + +// fixes issues with additional height coming from the shadow dom +input::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +input::-webkit-datetime-edit-ampm-field, +input::-webkit-datetime-edit-day-field, +input::-webkit-datetime-edit-hour-field, +input::-webkit-datetime-edit-millisecond-field, +input::-webkit-datetime-edit-minute-field, +input::-webkit-datetime-edit-month-field, +input::-webkit-datetime-edit-second-field, +input::-webkit-datetime-edit-week-field, +input::-webkit-datetime-edit-year-field { + padding-top: 0; + padding-bottom: 0; +} + +input[type="date"]::-webkit-inner-spin-button, +input[type="datetime"]::-webkit-inner-spin-button, +input[type="datetime-local"]::-webkit-inner-spin-button, +input[type="month"]::-webkit-inner-spin-button, +input[type="time"]::-webkit-inner-spin-button, +input[type="week"]::-webkit-inner-spin-button { + height: 100%; +} + +input[type="search"]::-webkit-search-cancel-button { + display: none; +} + +:-moz-ui-invalid:not(output) { + box-shadow: none; +} + +button::-moz-focus-inner { + border: 0; +} diff --git a/Styles/less/core/reset.less b/Styles/less/core/reset.less new file mode 100644 index 0000000..05b560f --- /dev/null +++ b/Styles/less/core/reset.less @@ -0,0 +1,49 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +a, abbr, address, area, article, aside, audio, b, bdi, bdo, blockquote, body, br, button, canvas, caption, cite, code, col, colgroup, datalist, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hr, html, i, iframe, img, input, ins, kbd, keygen, label, legend, li, map, mark, main, menu, meter, nav, object, ol, optgroup, option, output, p, param, pre, progress, q, rp, rt, ruby, s, samp, section, select, small, source, span, strong, sub, summary, sup, table, tbody, td, textarea, tfoot, th, thead, time, tr, track, u, ul, var, video, wbr, +@{custom-tags}, +::before, +::after { + box-sizing: inherit; + margin: 0; + padding: 0; + font: inherit; + line-height: inherit; + letter-spacing: inherit; + color: inherit; + background: none; + border: none; +} + +body { + box-sizing: border-box; +} + +ul, ol { + list-style-type: none; +} + +a { + text-decoration: none; +} + +:-moz-focusring { + outline: none; +} + +:focus { + outline: none; +} + +form-field { + display: inline-block; +} + +file-drop, +paginated-list, +paginated-view, +toast-message { + display: block; +} diff --git a/Styles/less/core/typography.less b/Styles/less/core/typography.less new file mode 100644 index 0000000..42880e4 --- /dev/null +++ b/Styles/less/core/typography.less @@ -0,0 +1,7 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +body { + font-family: @font-sans; +} diff --git a/Styles/less/elements/all.less b/Styles/less/elements/all.less new file mode 100644 index 0000000..7892a06 --- /dev/null +++ b/Styles/less/elements/all.less @@ -0,0 +1 @@ +@import 'form-error'; diff --git a/Styles/less/elements/form-error.less b/Styles/less/elements/form-error.less new file mode 100644 index 0000000..1f837a2 --- /dev/null +++ b/Styles/less/elements/form-error.less @@ -0,0 +1,11 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +form-error { + display: block; + + &[empty] { + display: none; + } +} diff --git a/Styles/less/fonts/vollkorn.less b/Styles/less/fonts/vollkorn.less new file mode 100644 index 0000000..a95a898 --- /dev/null +++ b/Styles/less/fonts/vollkorn.less @@ -0,0 +1,8 @@ +/** + * (c) 2016 Ruben Schmidmeister + */ + +@font-face { + font-family: 'Vollkorn'; + src: url('/fonts/vollkorn-medium-webfont.woff') +} diff --git a/Survey/Rakefile b/Survey/Rakefile new file mode 100644 index 0000000..8aef2f6 --- /dev/null +++ b/Survey/Rakefile @@ -0,0 +1,18 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Survey/bootstrap.php b/Survey/bootstrap.php new file mode 100644 index 0000000..88c2060 --- /dev/null +++ b/Survey/bootstrap.php @@ -0,0 +1,5 @@ + + + + + + + + + + + + + +

+ For full functionality it is necessary to enable JavaScript. +
+ + + +
+
+ + + + + diff --git a/Survey/index.php b/Survey/index.php new file mode 100644 index 0000000..163c53a --- /dev/null +++ b/Survey/index.php @@ -0,0 +1,13 @@ +run(); +} diff --git a/Survey/src/Bootstrap/Bootstrapper.php b/Survey/src/Bootstrap/Bootstrapper.php new file mode 100644 index 0000000..484a6ca --- /dev/null +++ b/Survey/src/Bootstrap/Bootstrapper.php @@ -0,0 +1,99 @@ +bootstrapStreamWrappers(); + $this->bootstrapSession(); + } + + private function bootstrapStreamWrappers() + { + TemplatesStreamWrapper::setUp(__DIR__ . '/../../data/templates'); + } + + private function bootstrapSession() + { + /** @var Session $session */ + $session = $this->getFactory()->createSession(); + + $session->loadRequest($this->getRequest()); + } + + protected function buildConfiguration(): ConfigurationInterface + { + return new Configuration(__DIR__ . '/../../config/system.ini'); + } + + protected function buildFactory(): MasterFactoryInterface + { + $factory = new MasterFactory($this->getConfiguration()); + + $factory->registerFactory(new \Timetabio\Framework\Factories\FrameworkFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\BackendFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\LoggerFactory); + + $factory->registerFactory(new \Timetabio\Library\Factories\LocatorFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\ServiceFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\MapperFactory); + + $factory->registerFactory(new \Timetabio\Frontend\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\ErrorHandlerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\ControllerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\HandlerFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\RendererFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\TransformationFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\RouterFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\QueryFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\LocatorFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\SessionFactory); + $factory->registerFactory(new \Timetabio\Frontend\Factories\CommandFactory); + + $factory->registerFactory(new \Timetabio\Survey\Factories\QueryFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\RouterFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\HandlerFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\ControllerFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\RendererFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\CommandFactory); + $factory->registerFactory(new \Timetabio\Survey\Factories\ApplicationFactory); + + return $factory; + } + + protected function buildRouter(): RouterInterface + { + $router = new Router; + + $router->addRouter($this->getFactory()->createBetaSurveyRouter()); + $router->addRouter($this->getFactory()->createSurveyActionRouter()); + $router->addRouter($this->getFactory()->createNotFoundRouter()); + + return $router; + } + + protected function buildErrorHandler(): AbstractErrorHandler + { + if ($this->getConfiguration()->isDevelopmentMode()) { + return $this->getFactory()->createDevelopmentErrorHandler(); + } + + return $this->getFactory()->createProductionErrorHandler(); + } + } +} diff --git a/Survey/src/Builders/UriBuilder.php b/Survey/src/Builders/UriBuilder.php new file mode 100644 index 0000000..b336016 --- /dev/null +++ b/Survey/src/Builders/UriBuilder.php @@ -0,0 +1,24 @@ +uriHost = $uriHost; + } + + public function buildSurveyThanksPage(): string + { + return $this->uriHost . '/survey/thanks'; + } + } +} diff --git a/Survey/src/Commands/ApproveBetaRequestCommand.php b/Survey/src/Commands/ApproveBetaRequestCommand.php new file mode 100644 index 0000000..89350ee --- /dev/null +++ b/Survey/src/Commands/ApproveBetaRequestCommand.php @@ -0,0 +1,33 @@ +databaseBackend = $databaseBackend; + } + + public function execute(string $betaRequest) + { + $this->databaseBackend->execute( + 'UPDATE beta_requests + SET approved = TRUE, survey_before_completed = TRUE + WHERE id = :id', + [ + 'id' => $betaRequest + ] + ); + } + } +} diff --git a/Survey/src/Commands/InsertAnswerCommand.php b/Survey/src/Commands/InsertAnswerCommand.php new file mode 100644 index 0000000..7d7a7ac --- /dev/null +++ b/Survey/src/Commands/InsertAnswerCommand.php @@ -0,0 +1,35 @@ +databaseBackend = $databaseBackend; + } + + public function execute(string $question, int $value, string $betaRequest) + { + return $this->databaseBackend->insert( + 'INSERT INTO survey_answers (value, survey_question_id, beta_request_id, version) + VALUES (:value, :survey_question_id, :beta_request_id, :version)', + [ + 'value' => $value, + 'survey_question_id' => $question, + 'beta_request_id' => $betaRequest, + 'version' => 'before' + ] + ); + } + } +} diff --git a/Survey/src/DataObjects/Question.php b/Survey/src/DataObjects/Question.php new file mode 100644 index 0000000..b854a4b --- /dev/null +++ b/Survey/src/DataObjects/Question.php @@ -0,0 +1,29 @@ +question = $question; + } + + public function getId(): string + { + return $this->question['id']; + } + + public function getTitle(): string + { + return $this->question['title']; + } + } +} diff --git a/Survey/src/Factories/ApplicationFactory.php b/Survey/src/Factories/ApplicationFactory.php new file mode 100644 index 0000000..f6f0276 --- /dev/null +++ b/Survey/src/Factories/ApplicationFactory.php @@ -0,0 +1,18 @@ +getMasterFactory()->getConfiguration()->get('uriHost') + ); + } + } +} diff --git a/Survey/src/Factories/CommandFactory.php b/Survey/src/Factories/CommandFactory.php new file mode 100644 index 0000000..dc88f5d --- /dev/null +++ b/Survey/src/Factories/CommandFactory.php @@ -0,0 +1,25 @@ +getMasterFactory()->createPostgresBackend() + ); + } + + public function createInsertAnswerCommand(): \Timetabio\Survey\Commands\InsertAnswerCommand + { + return new \Timetabio\Survey\Commands\InsertAnswerCommand( + $this->getMasterFactory()->createPostgresBackend() + ); + } + } +} diff --git a/Survey/src/Factories/ControllerFactory.php b/Survey/src/Factories/ControllerFactory.php new file mode 100644 index 0000000..995f1ba --- /dev/null +++ b/Survey/src/Factories/ControllerFactory.php @@ -0,0 +1,43 @@ +getMasterFactory()->createGetPagePreHandler(), + $this->getMasterFactory()->createRequestHandler(), + $this->getMasterFactory()->createSurveyPageQueryHandler(), + $this->getMasterFactory()->createCommandHandler(), + $this->getMasterFactory()->createSurveyPageTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + public function createSurveyActionController(): \Timetabio\Framework\Controllers\PostController + { + return new \Timetabio\Framework\Controllers\PostController( + new \Timetabio\Survey\Models\Action\SurveyActionModel, + $this->getMasterFactory()->createPostPreHandler(), + $this->getMasterFactory()->createSurveyActionRequestHandler(), + $this->getMasterFactory()->createSurveyActionQueryHandler(), + $this->getMasterFactory()->createSurveyActionCommandHandler(), + $this->getMasterFactory()->createPostTransformationHandler(), + $this->getMasterFactory()->createResponseHandler(), + $this->getMasterFactory()->createPostHandler(), + new HtmlResponse + ); + } + + } +} diff --git a/Survey/src/Factories/HandlerFactory.php b/Survey/src/Factories/HandlerFactory.php new file mode 100644 index 0000000..1ee9179 --- /dev/null +++ b/Survey/src/Factories/HandlerFactory.php @@ -0,0 +1,47 @@ +getMasterFactory()->createSurveyPageRenderer() + ); + } + + public function createSurveyPageQueryHandler(): \Timetabio\Survey\Handlers\Get\SurveyPage\QueryHandler + { + return new \Timetabio\Survey\Handlers\Get\SurveyPage\QueryHandler( + $this->getMasterFactory()->createFetchQuestionsQuery() + ); + } + + public function createSurveyActionRequestHandler(): \Timetabio\Survey\Handlers\Post\Survey\RequestHandler + { + return new \Timetabio\Survey\Handlers\Post\Survey\RequestHandler; + } + + public function createSurveyActionQueryHandler(): \Timetabio\Survey\Handlers\Post\Survey\QueryHandler + { + return new \Timetabio\Survey\Handlers\Post\Survey\QueryHandler( + $this->getMasterFactory()->createFetchBetaRequestQuery(), + $this->getMasterFactory()->createFetchQuestionsQuery(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createSurveyActionCommandHandler(): \Timetabio\Survey\Handlers\Post\Survey\CommandHandler + { + return new \Timetabio\Survey\Handlers\Post\Survey\CommandHandler( + $this->getMasterFactory()->createApproveBetaRequestCommand(), + $this->getMasterFactory()->createInsertAnswerCommand() + ); + } + } +} diff --git a/Survey/src/Factories/QueryFactory.php b/Survey/src/Factories/QueryFactory.php new file mode 100644 index 0000000..0a0c534 --- /dev/null +++ b/Survey/src/Factories/QueryFactory.php @@ -0,0 +1,25 @@ +getMasterFactory()->createPostgresBackend() + ); + } + + public function createFetchQuestionsQuery(): \Timetabio\Survey\Queries\FetchQuestionsQuery + { + return new \Timetabio\Survey\Queries\FetchQuestionsQuery( + $this->getMasterFactory()->createPostgresBackend() + ); + } + } +} diff --git a/Survey/src/Factories/RendererFactory.php b/Survey/src/Factories/RendererFactory.php new file mode 100644 index 0000000..0cdab7a --- /dev/null +++ b/Survey/src/Factories/RendererFactory.php @@ -0,0 +1,32 @@ +getTemplate(), + $this->getMasterFactory()->createSurveyPageContentRenderer(), + $this->getMasterFactory()->createTransformer() + ); + } + + public function createSurveyPageContentRenderer(): \Timetabio\Survey\Renderers\PageContent\SurveyPageContentRenderer + { + return new \Timetabio\Survey\Renderers\PageContent\SurveyPageContentRenderer; + } + + private function getTemplate(): \Timetabio\Framework\Dom\Document + { + return $this->getMasterFactory()->createDomBackend()->loadHtml( + 'templates://template.html' + ); + } + } +} diff --git a/Survey/src/Factories/RouterFactory.php b/Survey/src/Factories/RouterFactory.php new file mode 100644 index 0000000..5ca7f12 --- /dev/null +++ b/Survey/src/Factories/RouterFactory.php @@ -0,0 +1,26 @@ +getMasterFactory(), + $this->getMasterFactory()->createFetchBetaRequestQuery() + ); + } + + public function createSurveyActionRouter(): \Timetabio\Survey\Routers\ActionRouter + { + return new \Timetabio\Survey\Routers\ActionRouter( + $this->getMasterFactory() + ); + } + } +} diff --git a/Survey/src/Handlers/Get/SurveyPage/QueryHandler.php b/Survey/src/Handlers/Get/SurveyPage/QueryHandler.php new file mode 100644 index 0000000..1cc8b37 --- /dev/null +++ b/Survey/src/Handlers/Get/SurveyPage/QueryHandler.php @@ -0,0 +1,32 @@ +fetchQuestionsQuery = $fetchQuestionsQuery; + } + + public function execute(AbstractModel $model) + { + /** @var SurveyPageModel $model */ + + $model->setTitle('Survey'); + $model->setQuestions($this->fetchQuestionsQuery->execute()); + } + } +} diff --git a/Survey/src/Handlers/Post/Survey/CommandHandler.php b/Survey/src/Handlers/Post/Survey/CommandHandler.php new file mode 100644 index 0000000..1e4e562 --- /dev/null +++ b/Survey/src/Handlers/Post/Survey/CommandHandler.php @@ -0,0 +1,44 @@ +approveBetaRequestCommand = $approveBetaRequestCommand; + $this->insertAnswerCommand = $insertAnswerCommand; + } + + public function execute(AbstractModel $model) + { + /** @var SurveyActionModel $model */ + + $betaRequest = $model->getBetaRequest(); + + foreach ($model->getAnswers() as $id => $value) { + $this->insertAnswerCommand->execute($id, $value, $betaRequest); + } + + $this->approveBetaRequestCommand->execute($betaRequest); + } + } +} diff --git a/Survey/src/Handlers/Post/Survey/QueryHandler.php b/Survey/src/Handlers/Post/Survey/QueryHandler.php new file mode 100644 index 0000000..f95a9e4 --- /dev/null +++ b/Survey/src/Handlers/Post/Survey/QueryHandler.php @@ -0,0 +1,75 @@ +fetchBetaRequestQuery = $fetchBetaRequestQuery; + $this->fetchQuestionsQuery = $fetchQuestionsQuery; + $this->uriBuilder = $uriBuilder; + } + + public function execute(AbstractModel $model) + { + /** @var SurveyActionModel $model */ + + $betaRequestId = $model->getBetaRequest(); + $rawAnswers = $model->getRawAnswers(); + $betaRequest = $this->fetchBetaRequestQuery->execute($betaRequestId); + + if ($betaRequest === null || $betaRequest['survey_before_completed']) { + throw new BadRequest('beta request not found'); + } + + $questions = $this->fetchQuestionsQuery->execute(); + + foreach ($questions as $question) { + $id = $question['id']; + + if (!isset($rawAnswers[$id])) { + throw new BadRequest('missing answer'); + } + + try { + $value = new AnswerValue($rawAnswers[$id]); + } catch (\Exception $exception) { + throw new BadRequest('invalid answer value'); + } + + $model->addAnswer($id, $value->getValue()); + } + + $model->setData([ + 'redirect' => $this->uriBuilder->buildSurveyThanksPage() + ]); + } + } +} diff --git a/Survey/src/Handlers/Post/Survey/RequestHandler.php b/Survey/src/Handlers/Post/Survey/RequestHandler.php new file mode 100644 index 0000000..b7624fb --- /dev/null +++ b/Survey/src/Handlers/Post/Survey/RequestHandler.php @@ -0,0 +1,29 @@ +setRawAnswers($request->getParam('answers')); + $model->setBetaRequest($request->getParam('beta_request')); + } catch (\Exception $exception) { + throw new BadRequest('missing or invalid parameters'); + } + } + } +} diff --git a/Survey/src/Models/Action/SurveyActionModel.php b/Survey/src/Models/Action/SurveyActionModel.php new file mode 100644 index 0000000..f3f6e0d --- /dev/null +++ b/Survey/src/Models/Action/SurveyActionModel.php @@ -0,0 +1,71 @@ +betaRequest; + } + + public function setBetaRequest(string $betaRequest) + { + $this->betaRequest = $betaRequest; + } + + public function getQuestions(): array + { + return $this->questions; + } + + public function setQuestions(array $questions) + { + $this->questions = $questions; + } + + public function getAnswers(): array + { + return $this->answers; + } + + public function addAnswer(string $id, int $value) + { + $this->answers[$id] = $value; + } + + public function getRawAnswers(): array + { + return $this->rawAnswers; + } + + public function setRawAnswers(array $rawAnswers) + { + $this->rawAnswers = $rawAnswers; + } + } +} diff --git a/Survey/src/Models/Page/SurveyPageModel.php b/Survey/src/Models/Page/SurveyPageModel.php new file mode 100644 index 0000000..dc50521 --- /dev/null +++ b/Survey/src/Models/Page/SurveyPageModel.php @@ -0,0 +1,41 @@ +betaRequest = $betaRequest; + } + + public function getBetaRequest(): array + { + return $this->betaRequest; + } + + public function getQuestions(): array + { + return $this->questions; + } + + public function setQuestions(array $questions) + { + $this->questions = $questions; + } + } +} diff --git a/Survey/src/Queries/FetchBetaRequestQuery.php b/Survey/src/Queries/FetchBetaRequestQuery.php new file mode 100644 index 0000000..c7b8548 --- /dev/null +++ b/Survey/src/Queries/FetchBetaRequestQuery.php @@ -0,0 +1,31 @@ +databaseBackend = $databaseBackend; + } + + public function execute(string $id) + { + return $this->databaseBackend->fetch( + 'SELECT * FROM beta_requests WHERE id = :id', + [ + 'id' => $id + ] + ); + } + } +} diff --git a/Survey/src/Queries/FetchQuestionsQuery.php b/Survey/src/Queries/FetchQuestionsQuery.php new file mode 100644 index 0000000..a20ee39 --- /dev/null +++ b/Survey/src/Queries/FetchQuestionsQuery.php @@ -0,0 +1,28 @@ +databaseBackend = $databaseBackend; + } + + public function execute(): array + { + return $this->databaseBackend->fetchAll( + 'SELECT * FROM survey_questions' + ); + } + } +} diff --git a/Survey/src/Renderers/PageContent/SurveyPageContentRenderer.php b/Survey/src/Renderers/PageContent/SurveyPageContentRenderer.php new file mode 100644 index 0000000..5f74763 --- /dev/null +++ b/Survey/src/Renderers/PageContent/SurveyPageContentRenderer.php @@ -0,0 +1,104 @@ + 'Strongly Agree', + 1 => 'Agree', + 0 => 'Neither agree or disagree', + -1 => 'Disagree', + -2 => 'Strongly disagree', + ]; + + public function render(PageModel $model, Document $template) + { + /** @var SurveyPageModel $model */ + + $questions = $model->getQuestions(); + $main = $template->getMainElement(); + + $wrapper = $template->createElement('div'); + $wrapper->setClassName('page-wrapper -padding'); + $main->appendChild($wrapper); + + $logoContainer = $template->createElement('div'); + $logoContainer->setClassName('flex-container -column -center-items'); + $wrapper->appendChild($logoContainer); + + $logoLink = $template->createElement('a'); + $logoLink->setClassName('_margin-after-l'); + $logoLink->setAttribute('href', '/'); + $logoContainer->appendChild($logoLink); + + $logoImage = $template->createElement('img'); + $logoImage->setAttribute('src', '/images/logo.svg'); + $logoImage->setAttribute('width', '60px'); + $logoImage->setAttribute('height', '60px'); + $logoLink->appendChild($logoImage); + + $form = $template->createElement('form'); + $form->setAttribute('is', 'ajax-form'); + $form->setAttribute('action', '/action/survey'); + $form->setAttribute('method', 'post'); + $wrapper->appendChild($form); + + foreach ($questions as $question) { + $questionCard = $template->createElement('div'); + $questionCard->setClassName('survey-question-card'); + $form->appendChild($questionCard); + + $questionTitle = $template->createElement('h2'); + $questionTitle->setClassName('title'); + $questionTitle->appendText($question['title']); + $questionCard->appendChild($questionTitle); + + $inputName = 'answers[' . $question['id'] . ']'; + + foreach ($this->choices as $choice => $label) { + $choiceElement = $template->createElement('label'); + $choiceElement->setClassName('choice'); + $questionCard->appendChild($choiceElement); + + $choiceInput = $template->createElement('input'); + $choiceInput->setClassName('input'); + $choiceInput->setAttribute('type', 'radio'); + $choiceInput->setAttribute('required', ''); + $choiceInput->setAttribute('value', $choice); + $choiceInput->setAttribute('name', $inputName); + $choiceElement->appendChild($choiceInput); + + $choiceLabel = $template->createElement('span'); + $choiceLabel->setClassName('label'); + $choiceLabel->appendText($label); + $choiceElement->appendChild($choiceLabel); + } + } + + $betaRequest = $template->createElement('input'); + $betaRequest->setAttribute('type', 'hidden'); + $betaRequest->setAttribute('name', 'beta_request'); + $betaRequest->setAttribute('value', $model->getBetaRequest()['id']); + $form->appendChild($betaRequest); + + $submitButton = $template->createElement('button'); + $submitButton->setClassName('basic-button'); + $submitButton->setAttribute('type', 'submit'); + $submitButton->setAttribute('disabled', ''); + $submitButton->appendText('Submit'); + + $form->appendChild($submitButton); + } + } +} diff --git a/Survey/src/Routers/ActionRouter.php b/Survey/src/Routers/ActionRouter.php new file mode 100644 index 0000000..4e82235 --- /dev/null +++ b/Survey/src/Routers/ActionRouter.php @@ -0,0 +1,40 @@ +factory = $factory; + } + + public function route(RequestInterface $request): ControllerInterface + { + switch ($request->getUri()->getPath()) { + case '/action/survey': + return $this->factory->createSurveyActionController(); + } + + throw new RouterException; + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\PostRequest; + } + } +} diff --git a/Survey/src/Routers/BetaSurveyRouter.php b/Survey/src/Routers/BetaSurveyRouter.php new file mode 100644 index 0000000..08e597e --- /dev/null +++ b/Survey/src/Routers/BetaSurveyRouter.php @@ -0,0 +1,54 @@ +factory = $factory; + $this->fetchBetaRequestQuery = $fetchBetaRequestQuery; + } + + public function route(RequestInterface $request): ControllerInterface + { + $parts = $request->getUri()->getExplodedPath(); + + if ($parts[0] !== 'beta' || count($parts) !== 2) { + throw new RouterException; + } + + $betaRequest = $this->fetchBetaRequestQuery->execute($parts[1]); + + if ($betaRequest === null || $betaRequest['survey_before_completed']) { + throw new RouterException; + } + + return $this->factory->createSurveyPageController($betaRequest); + } + + public function canHandle(RequestInterface $request): bool + { + return $request instanceof \Timetabio\Framework\Http\Request\GetRequest; + } + } +} diff --git a/Survey/src/ValueObjects/AnswerValue.php b/Survey/src/ValueObjects/AnswerValue.php new file mode 100644 index 0000000..160320a --- /dev/null +++ b/Survey/src/ValueObjects/AnswerValue.php @@ -0,0 +1,28 @@ + 2 || $value < -2) { + throw new \Exception('invalid answer value'); + } + + $this->value = $value; + } + + public function getValue(): int + { + return $this->value; + } + } +} diff --git a/Worker/Rakefile b/Worker/Rakefile new file mode 100644 index 0000000..8aef2f6 --- /dev/null +++ b/Worker/Rakefile @@ -0,0 +1,18 @@ +require 'rake/clean' +require '../rake/gen_autoload' + +TARGETS = [ + gen_autoload('src') +] + +task default: TARGETS + +desc 'Run tests' +task :test do + # run tests here +end + +desc 'Install dependencies' +task :deps do + # install dependencies here +end diff --git a/Worker/bootstrap.php b/Worker/bootstrap.php new file mode 100644 index 0000000..83a7cfc --- /dev/null +++ b/Worker/bootstrap.php @@ -0,0 +1,4 @@ + + + + + timetab.io + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + timetab.io + +
+

+ Hi there +

+
+

+ You have been invited to the feed "". +

+
+

+ If you want to accept this invitation hit "Add", otherwise you can safely delete this mail. +

+
+ + + + + + +
+ + Add "" + +
+
+

+ Alternatively: + +

+
+ + diff --git a/Worker/data/mails/verification.xhtml b/Worker/data/mails/verification.xhtml new file mode 100644 index 0000000..784203d --- /dev/null +++ b/Worker/data/mails/verification.xhtml @@ -0,0 +1,60 @@ + + + + + timetab.io + + + + + + + + timetab.io + + + + + + + + + + + + + +
+

+ Welcome! +

+ +

+ Please activate your account by confirming this email address. +

+
+ + + + + + +
+ + Activate Now + +
+
+

+ Alternatively: +

+ +

+ +

+
+ + diff --git a/Worker/data/pages/404.json b/Worker/data/pages/404.json new file mode 100644 index 0000000..2199591 --- /dev/null +++ b/Worker/data/pages/404.json @@ -0,0 +1,6 @@ +{ + "title": "Not Found", + "content": "content/404.html", + "code": 404, + "header": false +} diff --git a/Worker/data/pages/beta-thanks.json b/Worker/data/pages/beta-thanks.json new file mode 100644 index 0000000..e8429be --- /dev/null +++ b/Worker/data/pages/beta-thanks.json @@ -0,0 +1,5 @@ +{ + "title": "Beta", + "content": "content/beta/thanks.html", + "header": false +} diff --git a/Worker/data/pages/beta.json b/Worker/data/pages/beta.json new file mode 100644 index 0000000..cf5c49b --- /dev/null +++ b/Worker/data/pages/beta.json @@ -0,0 +1,5 @@ +{ + "title": "Beta", + "content": "content/beta/request.html", + "header": false +} diff --git a/Worker/data/pages/create-feed.json b/Worker/data/pages/create-feed.json new file mode 100644 index 0000000..9445442 --- /dev/null +++ b/Worker/data/pages/create-feed.json @@ -0,0 +1,4 @@ +{ + "title": "Create Feed", + "content": "content/account/create-feed.html" +} diff --git a/Worker/data/pages/error.json b/Worker/data/pages/error.json new file mode 100644 index 0000000..cc6b134 --- /dev/null +++ b/Worker/data/pages/error.json @@ -0,0 +1,6 @@ +{ + "title": "Internal Server Error", + "content": "content/error.html", + "code": 500, + "header": false +} diff --git a/Worker/data/pages/homepage.json b/Worker/data/pages/homepage.json new file mode 100644 index 0000000..2ff9059 --- /dev/null +++ b/Worker/data/pages/homepage.json @@ -0,0 +1,4 @@ +{ + "title": "Welcome", + "content": "content/homepage.html" +} diff --git a/Worker/data/pages/login.json b/Worker/data/pages/login.json new file mode 100644 index 0000000..452a597 --- /dev/null +++ b/Worker/data/pages/login.json @@ -0,0 +1,5 @@ +{ + "title": "Sign In", + "content": "content/login.html", + "header": false +} diff --git a/Worker/data/pages/register-confirmation.json b/Worker/data/pages/register-confirmation.json new file mode 100644 index 0000000..e201391 --- /dev/null +++ b/Worker/data/pages/register-confirmation.json @@ -0,0 +1,5 @@ +{ + "title": "Register", + "content": "content/register-confirmation.html", + "header": false +} diff --git a/Worker/data/pages/register.json b/Worker/data/pages/register.json new file mode 100644 index 0000000..bc62a64 --- /dev/null +++ b/Worker/data/pages/register.json @@ -0,0 +1,5 @@ +{ + "title": "Register", + "content": "content/register.html", + "header": false +} diff --git a/Worker/data/pages/resend-verification.json b/Worker/data/pages/resend-verification.json new file mode 100644 index 0000000..3c41d41 --- /dev/null +++ b/Worker/data/pages/resend-verification.json @@ -0,0 +1,5 @@ +{ + "title": "Resend Verification", + "content": "content/resend-verification.html", + "header": false +} diff --git a/Worker/data/pages/survey-thanks.json b/Worker/data/pages/survey-thanks.json new file mode 100644 index 0000000..918f1d4 --- /dev/null +++ b/Worker/data/pages/survey-thanks.json @@ -0,0 +1,5 @@ +{ + "title": "Survey", + "content": "content/survey/thanks.html", + "header": false +} diff --git a/Worker/data/routes.json b/Worker/data/routes.json new file mode 100644 index 0000000..2459998 --- /dev/null +++ b/Worker/data/routes.json @@ -0,0 +1,13 @@ +{ + "/": "homepage", + "/404": "404", + "/error": "error", + "/register": "register", + "/register/confirmation": "register-confirmation", + "/login": "login", + "/register/resend-verification": "resend-verification", + "/account/feeds/new": "create-feed", + "/beta": "beta", + "/beta/thanks": "beta-thanks", + "/survey/thanks": "survey-thanks" +} diff --git a/Worker/data/templates/content/404.html b/Worker/data/templates/content/404.html new file mode 100644 index 0000000..a3ca2b0 --- /dev/null +++ b/Worker/data/templates/content/404.html @@ -0,0 +1,12 @@ +
+
+ + + + + +

Page not found

+ +

Head back to our homepage

+
+
diff --git a/Worker/data/templates/content/account/create-feed.html b/Worker/data/templates/content/account/create-feed.html new file mode 100644 index 0000000..466c1ed --- /dev/null +++ b/Worker/data/templates/content/account/create-feed.html @@ -0,0 +1,39 @@ +
+

+ Create a new Feed +

+ +
+ + + + + + + + + +
+
diff --git a/Worker/data/templates/content/beta/request.html b/Worker/data/templates/content/beta/request.html new file mode 100644 index 0000000..8bd85a0 --- /dev/null +++ b/Worker/data/templates/content/beta/request.html @@ -0,0 +1,29 @@ +
+
+ + + + + +

+ Request Beta Invite +

+ +
+ + + + + + +
+
+
diff --git a/Worker/data/templates/content/beta/thanks.html b/Worker/data/templates/content/beta/thanks.html new file mode 100644 index 0000000..a547b8f --- /dev/null +++ b/Worker/data/templates/content/beta/thanks.html @@ -0,0 +1,22 @@ +
+
+ + + + +

+ Request Beta Invite +

+ +
+

+ Thank you for your interest in timetab.io. + We'll be sending out invitations real soon. +

+

+ To stay tuned you can follow us on Twitter. +

+
+
+
+ diff --git a/Worker/data/templates/content/error.html b/Worker/data/templates/content/error.html new file mode 100644 index 0000000..c8e81be --- /dev/null +++ b/Worker/data/templates/content/error.html @@ -0,0 +1,13 @@ +
+
+ + + + + +

Internal Server Error

+ +

Oops an unknown error occured.

+

The problem has automatically been reported for you.

+
+
diff --git a/Worker/data/templates/content/homepage.html b/Worker/data/templates/content/homepage.html new file mode 100644 index 0000000..ed6dc3d --- /dev/null +++ b/Worker/data/templates/content/homepage.html @@ -0,0 +1,3 @@ +
+

Welcome

+
diff --git a/Worker/data/templates/content/login.html b/Worker/data/templates/content/login.html new file mode 100644 index 0000000..7e3b083 --- /dev/null +++ b/Worker/data/templates/content/login.html @@ -0,0 +1,43 @@ +
+
+ + + + + +

+ Sign in to timetab.io +

+ +
+ + + + + + + + +
+ +

+ Don't have an account? + Register +

+
+
diff --git a/Worker/data/templates/content/register-confirmation.html b/Worker/data/templates/content/register-confirmation.html new file mode 100644 index 0000000..e35b153 --- /dev/null +++ b/Worker/data/templates/content/register-confirmation.html @@ -0,0 +1,23 @@ +
+
+ + + + + +

We're almost done

+ +
+

+ Please verify your email address by clicking the link in the email we sent out to you. +

+
+ +
+

+ Didn't receive an email? Resend +

+
+
+
+ diff --git a/Worker/data/templates/content/register.html b/Worker/data/templates/content/register.html new file mode 100644 index 0000000..4ec494e --- /dev/null +++ b/Worker/data/templates/content/register.html @@ -0,0 +1,51 @@ +
+
+ + + + + +

+ Create an Account +

+ +
+ + + + + + + + + + +
+ +

+ Already have an account? + Sign in +

+
+
diff --git a/Worker/data/templates/content/resend-verification.html b/Worker/data/templates/content/resend-verification.html new file mode 100644 index 0000000..b7f4a15 --- /dev/null +++ b/Worker/data/templates/content/resend-verification.html @@ -0,0 +1,27 @@ +
+
+ + + + + +

+ Resend Verification +

+ +
+ + + + + + +
+
+
diff --git a/Worker/data/templates/content/survey/thanks.html b/Worker/data/templates/content/survey/thanks.html new file mode 100644 index 0000000..3248ffd --- /dev/null +++ b/Worker/data/templates/content/survey/thanks.html @@ -0,0 +1,29 @@ +
+
+ + + + + +

Thank You

+ +
+

+ Thank you for taking the time to fill out our survey. +

+ +

+ We have approved your email for the private beta, so you can go ahead and + Sign Up. +

+ +

+ If you have any questions or feedback feel free to shoot us a mail at hey@timetab.io. +

+
+ +

+ tl;dr Create Account +

+
+
diff --git a/Worker/push.php b/Worker/push.php new file mode 100755 index 0000000..cfb628f --- /dev/null +++ b/Worker/push.php @@ -0,0 +1,26 @@ +#!/usr/bin/env php +getFactory(); + + /** @var TaskLocator $locator */ + $locator = $factory->createTaskLocator(); + + /** @var DataStoreWriter $dataStoreWriter */ + $dataStoreWriter = $factory->createDataStoreWriter(); + + $task = $locator->locate($argv[1]); + + $dataStoreWriter->queueTask($task); + + echo 'Pushed task ' . get_class($task) . ' to worker queue' . PHP_EOL; +} diff --git a/Worker/src/Bootstrapper.php b/Worker/src/Bootstrapper.php new file mode 100644 index 0000000..5afe72e --- /dev/null +++ b/Worker/src/Bootstrapper.php @@ -0,0 +1,70 @@ +configuration = $this->buildConfiguration(); + $this->factory = $this->buildFactory(); + + DataStreamWrapper::setUp(__DIR__ . '/../data'); + TemplatesStreamWrapper::setUp(__DIR__ . '/../data/templates'); + + (new Gettext())->setUp('messages', __DIR__ . '/../../Locale'); + } + + public function getFactory(): MasterFactoryInterface + { + return $this->factory; + } + + private function buildConfiguration(): ConfigurationInterface + { + return new Configuration(__DIR__ . '/../config/system.ini'); + } + + private function buildFactory(): MasterFactoryInterface + { + $factory = new MasterFactory($this->configuration); + + $factory->registerFactory(new \Timetabio\Framework\Factories\FrameworkFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\BackendFactory); + $factory->registerFactory(new \Timetabio\Framework\Factories\LoggerFactory); + + $factory->registerFactory(new \Timetabio\Library\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\LocatorFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\MapperFactory); + $factory->registerFactory(new \Timetabio\Library\Factories\IndexerFactory); + + $factory->registerFactory(new \Timetabio\Worker\Factories\ApplicationFactory); + $factory->registerFactory(new \Timetabio\Worker\Factories\MailFactory); + $factory->registerFactory(new \Timetabio\Worker\Factories\RunnerFactory); + $factory->registerFactory(new \Timetabio\Worker\Factories\RendererFactory); + $factory->registerFactory(new \Timetabio\Worker\Factories\LocatorFactory); + + return $factory; + } + } +} diff --git a/Worker/src/Builders/StaticPageBuilder.php b/Worker/src/Builders/StaticPageBuilder.php new file mode 100644 index 0000000..a91d50d --- /dev/null +++ b/Worker/src/Builders/StaticPageBuilder.php @@ -0,0 +1,70 @@ +fileBackend = $fileBackend; + $this->statusCodeLocator = $statusCodeLocator; + $this->staticPageRenderer = $staticPageRenderer; + $this->translator = $translator; + } + + public function build(string $name, LanguageInterface $language): StaticPage + { + $this->translator->setLanguage($language); + + $staticPage = json_decode($this->fileBackend->read('dataDir://pages/' . $name . '.json'), true); + + $content = $this->staticPageRenderer->render($staticPage['content']); + $statusCode = null; + $showHeader = true; + + if (isset($staticPage['code'])) { + $statusCode = $this->statusCodeLocator->locate($staticPage['code']); + } + + if (isset($staticPage['header'])) { + $showHeader = $staticPage['header']; + } + + return new StaticPage($staticPage['title'], $content, $statusCode, $showHeader); + } + } +} diff --git a/Worker/src/DataStore/DataStoreReader.php b/Worker/src/DataStore/DataStoreReader.php new file mode 100644 index 0000000..4cbaa34 --- /dev/null +++ b/Worker/src/DataStore/DataStoreReader.php @@ -0,0 +1,16 @@ +getDataStore()->get('post_text:' . $postId); + } + } +} diff --git a/Worker/src/DataStore/DataStoreWriter.php b/Worker/src/DataStore/DataStoreWriter.php new file mode 100644 index 0000000..f1f5a90 --- /dev/null +++ b/Worker/src/DataStore/DataStoreWriter.php @@ -0,0 +1,54 @@ +getDataStore()->remove('static_pages_' . $language); + } + + public function setStaticPage(string $name, LanguageInterface $language, StaticPage $staticPage) + { + $this->getDataStore()->setInHash('static_pages_' . $language, $name, serialize($staticPage)); + } + + public function removeStaticRoutes() + { + $this->getDataStore()->remove('static_routes'); + } + + public function setStaticRoute(string $route, string $staticPage) + { + $this->getDataStore()->setInHash('static_routes', $route, $staticPage); + } + + /** + * @deprecated + */ + public function setPostBody(string $postId, string $body) + { + $this->getDataStore()->set('post_body:' . $postId, $body); + } + + /** + * @deprecated + */ + public function setPostPreview(string $postId, string $body) + { + $this->getDataStore()->set('post_preview:' . $postId, $body); + } + + public function setPostText(string $postId, string $body) + { + $this->getDataStore()->set('post_text:' . $postId, $body); + } + } +} diff --git a/Worker/src/Factories/ApplicationFactory.php b/Worker/src/Factories/ApplicationFactory.php new file mode 100644 index 0000000..7a1ea86 --- /dev/null +++ b/Worker/src/Factories/ApplicationFactory.php @@ -0,0 +1,71 @@ +getMasterFactory()->createRedisBackend(), + $this->getMasterFactory()->createRunnerLocator() + ); + } + + public function createStaticPageBuilder(): \Timetabio\Worker\Builders\StaticPageBuilder + { + return new \Timetabio\Worker\Builders\StaticPageBuilder( + $this->getMasterFactory()->createFileBackend(), + $this->getMasterFactory()->createStatusCodeLocator(), + $this->getMasterFactory()->createStaticPageRenderer(), + $this->getMasterFactory()->createGettext() + ); + } + + public function createDataStoreWriter(): \Timetabio\Worker\DataStore\DataStoreWriter + { + return new \Timetabio\Worker\DataStore\DataStoreWriter( + $this->getMasterFactory()->createRedisBackend() + ); + } + + public function createDataStoreReader(): \Timetabio\Worker\DataStore\DataStoreReader + { + return new \Timetabio\Worker\DataStore\DataStoreReader( + $this->getMasterFactory()->createRedisBackend() + ); + } + + public function createFileService(): \Timetabio\Worker\Services\FileService + { + return new \Timetabio\Worker\Services\FileService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createPostService(): \Timetabio\Worker\Services\PostService + { + return new \Timetabio\Worker\Services\PostService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createUserService(): \Timetabio\Worker\Services\UserService + { + return new \Timetabio\Worker\Services\UserService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + + public function createFeedService(): \Timetabio\Worker\Services\FeedService + { + return new \Timetabio\Worker\Services\FeedService( + $this->getMasterFactory()->createPostgresBackend() + ); + } + } +} diff --git a/Worker/src/Factories/LocatorFactory.php b/Worker/src/Factories/LocatorFactory.php new file mode 100644 index 0000000..e3fc006 --- /dev/null +++ b/Worker/src/Factories/LocatorFactory.php @@ -0,0 +1,30 @@ +getMasterFactory() + ); + } + + public function createTaskLocator(): \Timetabio\Worker\Locators\TaskLocator + { + return new \Timetabio\Worker\Locators\TaskLocator( + $this->getMasterFactory() + ); + } + + public function createStatusCodeLocator(): \Timetabio\Worker\Locators\StatusCodeLocator + { + return new \Timetabio\Worker\Locators\StatusCodeLocator; + } + } +} diff --git a/Worker/src/Factories/MailFactory.php b/Worker/src/Factories/MailFactory.php new file mode 100644 index 0000000..e72ae33 --- /dev/null +++ b/Worker/src/Factories/MailFactory.php @@ -0,0 +1,27 @@ +getMasterFactory()->createDomBackend()->loadXml(__DIR__ . '/../../data/mails/verification.xhtml'), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createFeedInvitationMail(): \Timetabio\Worker\Mails\FeedInvitationMail + { + return new \Timetabio\Worker\Mails\FeedInvitationMail( + $this->getMasterFactory()->createDomBackend()->loadXml(__DIR__ . '/../../data/mails/invitation.xhtml'), + $this->getMasterFactory()->createUriBuilder() + ); + } + } +} diff --git a/Worker/src/Factories/RendererFactory.php b/Worker/src/Factories/RendererFactory.php new file mode 100644 index 0000000..2595ddf --- /dev/null +++ b/Worker/src/Factories/RendererFactory.php @@ -0,0 +1,26 @@ +getMasterFactory()->createGettext() + ); + } + + public function createStaticPageRenderer(): \Timetabio\Worker\Renderers\StaticPageRenderer + { + return new \Timetabio\Worker\Renderers\StaticPageRenderer( + $this->getMasterFactory()->createDomBackend(), + $this->getMasterFactory()->createTranslateTransformation() + ); + } + } +} diff --git a/Worker/src/Factories/RunnerFactory.php b/Worker/src/Factories/RunnerFactory.php new file mode 100644 index 0000000..741bf76 --- /dev/null +++ b/Worker/src/Factories/RunnerFactory.php @@ -0,0 +1,140 @@ +getMasterFactory()->createMailgunBackend(), + $this->getMasterFactory()->createVerificationMail() + ); + } + + public function createIndexUserRunner(): \Timetabio\Worker\Runners\IndexUserRunner + { + return new \Timetabio\Worker\Runners\IndexUserRunner( + $this->getMasterFactory()->createUserService(), + $this->getMasterFactory()->createUserIndexer() + ); + } + + public function createBuildStaticPagesRunner(): \Timetabio\Worker\Runners\BuildStaticPagesRunner + { + return new \Timetabio\Worker\Runners\BuildStaticPagesRunner( + $this->getMasterFactory()->createFileBackend(), + $this->getMasterFactory()->createDataStoreWriter(), + $this->getMasterFactory()->createStaticPageBuilder() + ); + } + + public function createInitialRunner(): \Timetabio\Worker\Runners\InitialRunner + { + return new \Timetabio\Worker\Runners\InitialRunner( + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createDeleteUnusedFilesRunner(): \Timetabio\Worker\Runners\DeleteUnusedFilesRunner + { + return new \Timetabio\Worker\Runners\DeleteUnusedFilesRunner( + $this->getMasterFactory()->createAwsRestBackend(), + $this->getMasterFactory()->createFileService(), + $this->getMasterFactory()->createUriBuilder() + ); + } + + public function createBuildPostsRunner(): \Timetabio\Worker\Runners\BuildPostsRunner + { + return new \Timetabio\Worker\Runners\BuildPostsRunner( + $this->getMasterFactory()->createDataStoreWriter(), + $this->getMasterFactory()->createPostService() + ); + } + + public function createBuildPostRunner(): \Timetabio\Worker\Runners\BuildPostRunner + { + return new \Timetabio\Worker\Runners\BuildPostRunner( + $this->getMasterFactory()->createPostService(), + $this->getMasterFactory()->createInkBackend(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createIndexPostsRunner(): \Timetabio\Worker\Runners\IndexPostsRunner + { + return new \Timetabio\Worker\Runners\IndexPostsRunner( + $this->getMasterFactory()->createDataStoreWriter(), + $this->getMasterFactory()->createPostService() + ); + } + + public function createIndexPostRunner(): \Timetabio\Worker\Runners\IndexPostRunner + { + return new \Timetabio\Worker\Runners\IndexPostRunner( + $this->getMasterFactory()->createPostService(), + $this->getMasterFactory()->createPostMapper(), + $this->getMasterFactory()->createDataStoreReader(), + $this->getMasterFactory()->createPostIndexer() + ); + } + + public function createSendFeedInvitationRunner(): \Timetabio\Worker\Runners\SendFeedInvitationRunner + { + return new \Timetabio\Worker\Runners\SendFeedInvitationRunner( + $this->getMasterFactory()->createUserService(), + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createMailgunBackend(), + $this->getMasterFactory()->createFeedInvitationMail() + ); + } + + public function createBuildFeedsRunner(): \Timetabio\Worker\Runners\BuildFeedsRunner + { + return new \Timetabio\Worker\Runners\BuildFeedsRunner( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createBuildFeedRunner(): \Timetabio\Worker\Runners\BuildFeedRunner + { + return new \Timetabio\Worker\Runners\BuildFeedRunner( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter(), + $this->getMasterFactory()->createUserRoleLocator() + ); + } + + public function createIndexFeedsRunner(): \Timetabio\Worker\Runners\IndexFeedsRunner + { + return new \Timetabio\Worker\Runners\IndexFeedsRunner( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createIndexFeedRunner(): \Timetabio\Worker\Runners\IndexFeedRunner + { + return new \Timetabio\Worker\Runners\IndexFeedRunner( + $this->getMasterFactory()->createFeedService(), + $this->getMasterFactory()->createFeedMapper(), + $this->getMasterFactory()->createFeedIndexer(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + + public function createIndexUsersRunner(): \Timetabio\Worker\Runners\IndexUsersRunner + { + return new \Timetabio\Worker\Runners\IndexUsersRunner( + $this->getMasterFactory()->createUserService(), + $this->getMasterFactory()->createDataStoreWriter() + ); + } + } +} diff --git a/Worker/src/Locators/RunnerLocator.php b/Worker/src/Locators/RunnerLocator.php new file mode 100644 index 0000000..7d2045c --- /dev/null +++ b/Worker/src/Locators/RunnerLocator.php @@ -0,0 +1,64 @@ +factory = $factory; + } + + public function locate(TaskInterface $task): RunnerInterface + { + $class = get_class($task); + + switch ($class) { + case Tasks\SendVerificationEmailTask::class: + return $this->factory->createSendVerificationEmailRunner(); + case Tasks\IndexUserTask::class: + return $this->factory->createIndexUserRunner(); + case Tasks\BuildStaticPagesTask::class: + return $this->factory->createBuildStaticPagesRunner(); + case Tasks\InitialTask::class: + return $this->factory->createInitialRunner(); + case Tasks\DeleteUnusedFilesTask::class: + return $this->factory->createDeleteUnusedFilesRunner(); + case Tasks\BuildPostsTask::class: + return $this->factory->createBuildPostsRunner(); + case Tasks\BuildPostTask::class: + return $this->factory->createBuildPostRunner(); + case Tasks\IndexPostsTask::class: + return $this->factory->createIndexPostsRunner(); + case Tasks\IndexPostTask::class: + return $this->factory->createIndexPostRunner(); + case Tasks\SendFeedInvitationTask::class: + return $this->factory->createSendFeedInvitationRunner(); + case Tasks\BuildFeedsTask::class: + return $this->factory->createBuildFeedsRunner(); + case Tasks\BuildFeedTask::class: + return $this->factory->createBuildFeedRunner(); + case Tasks\IndexFeedsTask::class: + return $this->factory->createIndexFeedsRunner(); + case Tasks\IndexFeedTask::class: + return $this->factory->createIndexFeedRunner(); + case Tasks\IndexUsersTask::class: + return $this->factory->createIndexUsersRunner(); + } + + throw new \RuntimeException('no runner found for task ' . $class); + } + } +} diff --git a/Worker/src/Locators/StatusCodeLocator.php b/Worker/src/Locators/StatusCodeLocator.php new file mode 100644 index 0000000..9da0287 --- /dev/null +++ b/Worker/src/Locators/StatusCodeLocator.php @@ -0,0 +1,23 @@ +template = $template; + } + + public function getRecipient(): EmailPerson + { + return $this->recipient; + } + + public function setRecipient(EmailPerson $recipient) + { + $this->recipient = $recipient; + } + + protected function getTemplate(): Document + { + return $this->template; + } + } +} diff --git a/Worker/src/Mails/FeedInvitationMail.php b/Worker/src/Mails/FeedInvitationMail.php new file mode 100644 index 0000000..db1b55f --- /dev/null +++ b/Worker/src/Mails/FeedInvitationMail.php @@ -0,0 +1,100 @@ +uriBuilder = $uriBuilder; + } + + public function setFeedName(string $feedName) + { + $this->feedName = $feedName; + } + + public function setFeedId(string $feedId) + { + $this->feedId = $feedId; + } + + public function setRole(UserRole $role) + { + $this->role = $role; + } + + public function getSubject(): string + { + return 'You have been invited to "' . $this->feedName . '"'; + } + + public function render(): string + { + $template = $this->getTemplate(); + $template->getXpath()->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml'); + + $feedUri = $this->uriBuilder->buildFeedPageUri($this->feedId); + + $greeting = $template->queryOne('//*[@id="greeting"]'); + $greeting->appendText($this->getRecipient()->getName()); + + $action = $template->queryOne('//*[@id="action"]'); + $action->appendText($this->getAction()); + + $feedNames = $template->query('//*[@class="feed-name"]'); + + /** @var Element $feedName */ + foreach ($feedNames as $feedName) { + $feedName->appendText($this->feedName); + } + + $button = $template->queryOne('//*[@id="button"]'); + $button->setAttribute('href', $feedUri); + + $link = $template->queryOne('//*[@id="link"]'); + $link->setAttribute('href', $feedUri); + $link->appendText($feedUri); + + return $template->saveHTML(); + } + + private function getAction(): string + { + if ($this->role instanceof \Timetabio\Library\UserRoles\Moderator) { + return 'moderate'; + } + + return 'add'; + } + } +} diff --git a/Worker/src/Mails/VerificationMail.php b/Worker/src/Mails/VerificationMail.php new file mode 100644 index 0000000..f7ee6be --- /dev/null +++ b/Worker/src/Mails/VerificationMail.php @@ -0,0 +1,56 @@ +uriBuilder = $uriBuilder; + } + + public function setToken(string $token) + { + $this->token = $token; + } + + public function getSubject(): string + { + return 'Verify your timetab.io account'; + } + + public function render(): string + { + $template = $this->getTemplate(); + $links = $template->getElementsByTagName('a'); + + $uri = $this->uriBuilder->buildVerificationUri($this->token); + + $links->item(1)->setAttribute('href', $uri); + + $alternate = $links->item(2); + + $alternate->setAttribute('href', $uri); + $alternate->appendChild($template->createTextNode($uri)); + + return $template->saveHTML(); + } + } +} diff --git a/Worker/src/Renderers/StaticPageRenderer.php b/Worker/src/Renderers/StaticPageRenderer.php new file mode 100644 index 0000000..84f8204 --- /dev/null +++ b/Worker/src/Renderers/StaticPageRenderer.php @@ -0,0 +1,37 @@ +domBackend = $domBackend; + $this->translateTransformation = $translateTransformation; + } + + public function render(string $content): string + { + $document = $this->domBackend->loadHtml('templates://' . $content); + + $this->translateTransformation->apply($document); + + return $document->saveHTML($document->documentElement); + } + } +} diff --git a/Worker/src/Runners/BuildFeedRunner.php b/Worker/src/Runners/BuildFeedRunner.php new file mode 100644 index 0000000..a5cdeeb --- /dev/null +++ b/Worker/src/Runners/BuildFeedRunner.php @@ -0,0 +1,69 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + $this->userRoleLocator = $userRoleLocator; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof BuildFeedTask) { + return; + } + + $feedId = $task->getFeedId(); + $feed = $this->feedService->getFullFeed($feedId); + + if ($feed === null) { + return; + } + + $this->dataStoreWriter->addFeed($feedId); + + if ($feed['is_private']) { + $this->dataStoreWriter->addPrivateFeed($feedId); + } + + if (isset($feed['vanity_name']) && !empty($feed['vanity_name'])) { + $this->dataStoreWriter->setVanity($feedId, $feed['vanity_name']); + } + + $users = $this->feedService->getFeedUsers($feedId); + + foreach ($users as $user) { + $userRole = $this->userRoleLocator->locate($user['role']); + + $this->dataStoreWriter->setFeedAccess($feedId, $user['user_id'], $userRole); + } + } + } +} diff --git a/Worker/src/Runners/BuildFeedsRunner.php b/Worker/src/Runners/BuildFeedsRunner.php new file mode 100644 index 0000000..0528d25 --- /dev/null +++ b/Worker/src/Runners/BuildFeedsRunner.php @@ -0,0 +1,41 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof BuildFeedsTask) { + return; + } + + foreach ($this->feedService->getFeedIds() as $feed) { + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\BuildFeedTask($feed)); + } + } + } +} diff --git a/Worker/src/Runners/BuildPostRunner.php b/Worker/src/Runners/BuildPostRunner.php new file mode 100644 index 0000000..3396790 --- /dev/null +++ b/Worker/src/Runners/BuildPostRunner.php @@ -0,0 +1,56 @@ +postService = $postService; + $this->inkBackend = $inkBackend; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof BuildPostTask) { + return; + } + + $post = $this->postService->getPostBody($task->getPostId()); + + if ($post === null) { + return; + } + + $rendered = $this->inkBackend->process($post['body']); + + $this->dataStoreWriter->setPostBody($post['id'], $rendered->getBody()); + $this->dataStoreWriter->setPostPreview($post['id'], $rendered->getPreview()); + $this->dataStoreWriter->setPostText($post['id'], $rendered->getPlainText()); + } + } +} diff --git a/Worker/src/Runners/BuildPostsRunner.php b/Worker/src/Runners/BuildPostsRunner.php new file mode 100644 index 0000000..a3de0c3 --- /dev/null +++ b/Worker/src/Runners/BuildPostsRunner.php @@ -0,0 +1,47 @@ +dataStoreWriter = $dataStoreWriter; + $this->postService = $postService; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof BuildPostsTask) { + return; + } + + $posts = $this->postService->getPostIds(); + + foreach ($posts as $post) { + $priority = new \Timetabio\Library\TaskPriorities\Random(7200); + + $this->dataStoreWriter->queueTask( + new \Timetabio\Library\Tasks\BuildPostTask($post, $priority) + ); + } + } + } +} diff --git a/Worker/src/Runners/BuildStaticPagesRunner.php b/Worker/src/Runners/BuildStaticPagesRunner.php new file mode 100644 index 0000000..fafbec4 --- /dev/null +++ b/Worker/src/Runners/BuildStaticPagesRunner.php @@ -0,0 +1,61 @@ +fileBackend = $fileBackend; + $this->dataStoreWriter = $dataStoreWriter; + $this->staticPageBuilder = $staticPageBuilder; + } + + public function run(TaskInterface $task) + { + $routes = json_decode($this->fileBackend->read('dataDir://routes.json'), true); + + $this->dataStoreWriter->removeStaticRoutes(); + + foreach ($routes as $route => $name) { + $this->build($route, $name, new \Timetabio\Framework\Languages\English); + // $this->build($route, $name, new \Timetabio\Framework\Languages\German); + } + } + + private function build(string $route, string $name, LanguageInterface $language) + { + $staticPage = $this->staticPageBuilder->build($name, $language); + + $this->dataStoreWriter->setStaticRoute($route, $name); + $this->dataStoreWriter->setStaticPage($name, $language, $staticPage); + } + } +} diff --git a/Worker/src/Runners/DeleteUnusedFilesRunner.php b/Worker/src/Runners/DeleteUnusedFilesRunner.php new file mode 100644 index 0000000..cbd7430 --- /dev/null +++ b/Worker/src/Runners/DeleteUnusedFilesRunner.php @@ -0,0 +1,55 @@ +awsRestBackend = $awsRestBackend; + $this->fileService = $fileService; + $this->uriBuilder = $uriBuilder; + } + + public function run(TaskInterface $task) + { + if (!($task instanceof DeleteUnusedFilesTask)) { + return; + } + + $files = $this->fileService->getUnusedFiles(20); + + foreach ($files as $file) { + $uri = $this->uriBuilder->buildFileUri($file['public_id'], $file['name']); + + echo 'Deleting file ' . $uri . PHP_EOL; + + $this->awsRestBackend->deleteObject($uri); + $this->fileService->deleteFile($file['id']); + } + } + } +} diff --git a/Worker/src/Runners/IndexFeedRunner.php b/Worker/src/Runners/IndexFeedRunner.php new file mode 100644 index 0000000..0393894 --- /dev/null +++ b/Worker/src/Runners/IndexFeedRunner.php @@ -0,0 +1,66 @@ +feedService = $feedService; + $this->feedMapper = $feedMapper; + $this->indexer = $indexer; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof IndexFeedTask) { + return; + } + + $feedId = $task->getFeedId(); + $feed = $this->feedService->getFeed($feedId); + + if ($feed === null) { + $this->indexer->deleteDocument($feedId); + return; + } + + $mapped = $this->feedMapper->map($feed); + $this->indexer->indexDocument($feedId, $mapped); + + foreach ($this->feedService->getFeedPostIds($feedId) as $post) { + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexPostTask($post)); + } + } + } +} diff --git a/Worker/src/Runners/IndexFeedsRunner.php b/Worker/src/Runners/IndexFeedsRunner.php new file mode 100644 index 0000000..494e92b --- /dev/null +++ b/Worker/src/Runners/IndexFeedsRunner.php @@ -0,0 +1,41 @@ +feedService = $feedService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof IndexFeedsTask) { + return; + } + + foreach ($this->feedService->getFeedIds() as $feed) { + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexFeedTask($feed)); + } + } + } +} diff --git a/Worker/src/Runners/IndexPostRunner.php b/Worker/src/Runners/IndexPostRunner.php new file mode 100644 index 0000000..2e2c8eb --- /dev/null +++ b/Worker/src/Runners/IndexPostRunner.php @@ -0,0 +1,68 @@ +postService = $postService; + $this->postMapper = $postMapper; + $this->dataStoreReader = $dataStoreReader; + $this->indexer = $indexer; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof IndexPostTask) { + return; + } + + $postId = $task->getPostId(); + + $post = $this->postService->getPost($postId); + + if ($post === null) { + $this->indexer->deleteDocument($postId); + return; + } + + $post['body'] = $this->dataStoreReader->getPostText($postId); + $post['rendered_body'] = $this->dataStoreReader->getPostBody($postId); + $post['preview'] = $this->dataStoreReader->getPostPreview($postId); + + $mapped = $this->postMapper->map($post); + + $this->indexer->indexDocument($postId, $mapped); + } + } +} diff --git a/Worker/src/Runners/IndexPostsRunner.php b/Worker/src/Runners/IndexPostsRunner.php new file mode 100644 index 0000000..bfde38a --- /dev/null +++ b/Worker/src/Runners/IndexPostsRunner.php @@ -0,0 +1,43 @@ +dataStoreWriter = $dataStoreWriter; + $this->postService = $postService; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof IndexPostsTask) { + return; + } + + $posts = $this->postService->getPostIds(); + + foreach ($posts as $post) { + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexPostTask($post)); + } + } + } +} diff --git a/Worker/src/Runners/IndexUserRunner.php b/Worker/src/Runners/IndexUserRunner.php new file mode 100644 index 0000000..df9b8b5 --- /dev/null +++ b/Worker/src/Runners/IndexUserRunner.php @@ -0,0 +1,44 @@ +userService = $userService; + $this->indexer = $indexer; + } + + public function run(TaskInterface $task) + { + if (!($task instanceof IndexUserTask)) { + return; + } + + $userId = $task->getUserId(); + $feeds = iterator_to_array($this->userService->getUserFeeds($userId)); + + $this->indexer->indexDocument($userId, [ + 'feeds' => $feeds + ]); + } + } +} diff --git a/Worker/src/Runners/IndexUsersRunner.php b/Worker/src/Runners/IndexUsersRunner.php new file mode 100644 index 0000000..8ea8ef9 --- /dev/null +++ b/Worker/src/Runners/IndexUsersRunner.php @@ -0,0 +1,41 @@ +userService = $userService; + $this->dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!($task instanceof IndexUsersTask)) { + return; + } + + foreach ($this->userService->getUserIds() as $user) { + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\IndexUserTask($user)); + } + } + } +} diff --git a/Worker/src/Runners/InitialRunner.php b/Worker/src/Runners/InitialRunner.php new file mode 100644 index 0000000..5b63b5b --- /dev/null +++ b/Worker/src/Runners/InitialRunner.php @@ -0,0 +1,33 @@ +dataStoreWriter = $dataStoreWriter; + } + + public function run(TaskInterface $task) + { + if (!($task instanceof InitialTask)) { + return; + } + + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\BuildStaticPagesTask); + $this->dataStoreWriter->queueTask(new \Timetabio\Library\Tasks\DeleteUnusedFilesTask); + } + } +} diff --git a/Worker/src/Runners/RunnerInterface.php b/Worker/src/Runners/RunnerInterface.php new file mode 100644 index 0000000..e248e7f --- /dev/null +++ b/Worker/src/Runners/RunnerInterface.php @@ -0,0 +1,13 @@ +userService = $userService; + $this->feedService = $feedService; + $this->mailBackend = $mailBackend; + $this->invitationMail = $invitationMail; + } + + public function run(TaskInterface $task) + { + if (!$task instanceof SendFeedInvitationTask) { + return; + } + + $invitation = $task->getInvitation(); + $feed = $this->feedService->getFeed($invitation->getFeedId()); + $user = $this->userService->getUser($invitation->getUserId()); + + $email = new EmailAddress($user['email']); + $displayName = new DisplayName($user); + + $this->invitationMail->setFeedId($feed['id']); + $this->invitationMail->setFeedName($feed['name']); + $this->invitationMail->setRecipient(new EmailPerson($email, $displayName)); + $this->invitationMail->setRole($invitation->getUserRole()); + + $this->mailBackend->send($this->invitationMail); + } + } +} diff --git a/Worker/src/Runners/SendVerificationEmailRunner.php b/Worker/src/Runners/SendVerificationEmailRunner.php new file mode 100644 index 0000000..7d98400 --- /dev/null +++ b/Worker/src/Runners/SendVerificationEmailRunner.php @@ -0,0 +1,42 @@ +mailBackend = $mailBackend; + $this->verificationMail = $verificationMail; + } + + public function run(TaskInterface $task) + { + if (!($task instanceof SendVerificationEmailTask)) { + return; + } + + $this->verificationMail->setRecipient($task->getPerson()); + $this->verificationMail->setToken($task->getToken()); + + $this->mailBackend->send($this->verificationMail); + } + } +} diff --git a/Worker/src/Services/FeedService.php b/Worker/src/Services/FeedService.php new file mode 100644 index 0000000..9849d27 --- /dev/null +++ b/Worker/src/Services/FeedService.php @@ -0,0 +1,69 @@ +postgresBackend = $postgresBackend; + } + + public function getFeedIds(): \Traversable + { + return $this->postgresBackend->fetchColumns('SELECT id FROM feeds'); + } + + public function getFullFeed(string $feedId) + { + return $this->postgresBackend->fetch( + 'SELECT feeds.*, feed_vanities.name AS vanity_name + FROM feeds + LEFT OUTER JOIN feed_vanities ON feeds.id = feed_vanities.feed_id + WHERE feeds.id = :id', + [ + 'id' => $feedId + ] + ); + } + + public function getFeedUsers(string $feedId): array + { + return $this->postgresBackend->fetchAll( + 'SELECT * FROM feed_users WHERE feed_id = :feed_id', + [ + 'feed_id' => $feedId + ] + ); + } + + public function getFeed(string $feedId) + { + return $this->postgresBackend->fetch( + 'SELECT * FROM feeds WHERE id = :id', + [ + 'id' => $feedId + ] + ); + } + + public function getFeedPostIds(string $feedId): \Traversable + { + return $this->postgresBackend->fetchColumns( + 'SELECT id FROM posts WHERE feed_id = :id', + [ + 'id' => $feedId + ] + ); + } + } +} diff --git a/Worker/src/Services/FileService.php b/Worker/src/Services/FileService.php new file mode 100644 index 0000000..c0fe93b --- /dev/null +++ b/Worker/src/Services/FileService.php @@ -0,0 +1,42 @@ +postgresBackend = $postgresBackend; + } + + public function getUnusedFiles(int $limit): array + { + return $this->postgresBackend->fetchAll( + 'SELECT files.* FROM files + LEFT JOIN post_attachments ON post_attachments.file_id = files.id + WHERE post_attachments.id IS NULL + AND files.created < (utc_now() - INTERVAL \'24 hours\') + LIMIT :limit', + [ + 'limit' => $limit + ] + ); + } + + public function deleteFile(string $fileId) + { + $this->postgresBackend->execute('DELETE FROM files WHERE id = :id', [ + 'id' => $fileId + ]); + } + } +} diff --git a/Worker/src/Services/PostService.php b/Worker/src/Services/PostService.php new file mode 100644 index 0000000..533c955 --- /dev/null +++ b/Worker/src/Services/PostService.php @@ -0,0 +1,40 @@ +postgresBackend = $postgresBackend; + } + + public function getPostIds(): \Traversable + { + return $this->postgresBackend->fetchColumns('SELECT id FROM posts'); + } + + public function getPostBody(string $postId) + { + return $this->postgresBackend->fetch('SELECT id, body FROM posts WHERE id = :id', [ + 'id' => $postId + ]); + } + + public function getPost(string $postId) + { + return $this->postgresBackend->fetch('SELECT * FROM aggregated_posts WHERE id = :id', [ + 'id' => $postId + ]); + } + } +} diff --git a/Worker/src/Services/UserService.php b/Worker/src/Services/UserService.php new file mode 100644 index 0000000..d40442f --- /dev/null +++ b/Worker/src/Services/UserService.php @@ -0,0 +1,49 @@ +postgresBackend = $postgresBackend; + } + + public function getUser(string $userId) + { + return $this->postgresBackend->fetch( + 'SELECT id, username, name, email FROM users WHERE id = :id', + [ + 'id' => $userId + ] + ); + } + + public function getUserFeeds(string $userId): \Traversable + { + return $this->postgresBackend->fetchColumns( + 'SELECT feeds.id + FROM feed_users + JOIN feeds ON feed_users.feed_id = feeds.id + WHERE feed_users.user_id = :user_id', + [ + 'user_id' => $userId + ] + ); + } + + public function getUserIds(): \Traversable + { + return $this->postgresBackend->fetchColumns('SELECT id FROM users'); + } + } +} diff --git a/Worker/src/Transformations/TransformationInterface.php b/Worker/src/Transformations/TransformationInterface.php new file mode 100644 index 0000000..6d42a5d --- /dev/null +++ b/Worker/src/Transformations/TransformationInterface.php @@ -0,0 +1,13 @@ +translator = $translator; + } + + public function apply(Document $template) + { + $translateElements = $template->query('//translate'); + + /** @var Element $element */ + foreach ($translateElements as $element) { + $key = $element->nodeValue; + $context = $element->getAttribute('context'); + + $message = $this->translator->translate($key, $context); + + $element->parentNode->replaceChild( + $template->createTextNode($message), + $element + ); + } + } + } +} diff --git a/Worker/src/Worker.php b/Worker/src/Worker.php new file mode 100644 index 0000000..11f40fd --- /dev/null +++ b/Worker/src/Worker.php @@ -0,0 +1,61 @@ +redisBackend = $redisBackend; + $this->runnerLocator = $runnerLocator; + } + + public function start() + { + do { + $this->process(); + sleep(1); + } while (true); + } + + private function process() + { + $event = unserialize($this->redisBackend->zLpop('task_queue')); + + if (!$event) { + return; + } + + echo 'Running task ' . get_class($event) . PHP_EOL; + + try { + $runner = $this->runnerLocator->locate($event); + $runner->run($event); + } catch (\Throwable $exception) { + $this->logger->emergency($exception); + + echo $exception->getMessage() . PHP_EOL; + echo $exception->getTraceAsString() . PHP_EOL; + } + } + } +} diff --git a/Worker/worker.php b/Worker/worker.php new file mode 100755 index 0000000..f13632d --- /dev/null +++ b/Worker/worker.php @@ -0,0 +1,13 @@ +#!/usr/bin/env php +getFactory() + ->createWorker() + ->start(); +} diff --git a/containers/ttio-api/Dockerfile b/containers/ttio-api/Dockerfile new file mode 100644 index 0000000..b839605 --- /dev/null +++ b/containers/ttio-api/Dockerfile @@ -0,0 +1,22 @@ +FROM docker.ttio.cloud:5000/library/fpm + +# Framework +COPY Framework/src /data/code/Framework/src +COPY Framework/lib /data/code/Framework/lib +COPY Framework/bootstrap.php /data/code/Framework/bootstrap.php + +# Library +COPY Library/src /data/code/Library/src +COPY Library/bootstrap.php /data/code/Library/bootstrap.php + +# Ink +COPY Ink/src /data/code/Ink/src + +# API +COPY config/live/api.ini /data/code/API/config/system.ini +COPY API/scripts /data/code/API/scripts +COPY API/src /data/code/API/src +COPY API/bootstrap.php /data/code/API/bootstrap.php +COPY API/index.php /data/code/API/index.php + +VOLUME /data/code diff --git a/containers/ttio-dev-frontend/Dockerfile b/containers/ttio-dev-frontend/Dockerfile new file mode 100644 index 0000000..8df789e --- /dev/null +++ b/containers/ttio-dev-frontend/Dockerfile @@ -0,0 +1,4 @@ +FROM docker.ttio.cloud:5000/web/frontend + +COPY ttio-root-ca.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates diff --git a/containers/ttio-dev-frontend/ttio-root-ca.crt b/containers/ttio-dev-frontend/ttio-root-ca.crt new file mode 100644 index 0000000..b76b2a6 --- /dev/null +++ b/containers/ttio-dev-frontend/ttio-root-ca.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ryiCW30M +-----END CERTIFICATE----- diff --git a/containers/ttio-dev-proxy/Dockerfile b/containers/ttio-dev-proxy/Dockerfile new file mode 100644 index 0000000..992a3f3 --- /dev/null +++ b/containers/ttio-dev-proxy/Dockerfile @@ -0,0 +1,9 @@ +FROM library/nginx:alpine + +COPY ./nginx/conf.d/* /etc/nginx/conf.d/ +COPY ./nginx/nginx.conf /etc/nginx/nginx.conf + +COPY ./certs /data/certs + +RUN mkdir -p /var/www +VOLUME /var/www diff --git a/containers/ttio-dev-proxy/certs/fullchain.pem b/containers/ttio-dev-proxy/certs/fullchain.pem new file mode 100644 index 0000000..758fd5b --- /dev/null +++ b/containers/ttio-dev-proxy/certs/fullchain.pem @@ -0,0 +1,98 @@ +-----BEGIN CERTIFICATE----- +MIIFEDCCAvigAwIBAgICIAMwDQYJKoZIhvcNAQELBQAwgYMxCzAJBgNVBAYTAkNI +MRQwEgYDVQQIDAtTd2l0emVybGFuZDETMBEGA1UECgwKdGltZXRhYi5pbzEqMCgG +A1UEAwwhdGltZXRhYi5pbyBJbnRlcm1lZGlhdGUgQXV0aG9yaXR5MR0wGwYJKoZI +hvcNAQkBFg5oZXlAdGltZXRhYi5pbzAeFw0xNjEyMTQxNDEzNDJaFw0xNzEyMjQx +NDEzNDJaMHAxCzAJBgNVBAYTAkNIMRQwEgYDVQQIDAtTd2l0emVybGFuZDETMBEG +A1UECgwKdGltZXRhYi5pbzEXMBUGA1UEAwwOZGV2LnRpbWV0YWIuaW8xHTAbBgkq +hkiG9w0BCQEWDmhleUB0aW1ldGFiLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAviSYeeMQvp1uiK4Yy2DH+FTLKQcxfOtqB07WGs4sbdWPD11YjJ// +Zln/qUicko/F1dyfh+lyaL69Ww4y5HA6DyGRa94VRnEigRmkbVJlR/KnX1/1L0fm +f5PL1wngS6shop/UTwqsSc3D3MeFMRXmrnhADoxF2fpDjNxdXRzXda7ChhGZp5qU +QbE/AKr57HLTuOB4bnVE8ntTauImAy1hGFOQTlf6bmMM+X+tmMmHcBC5dSIaNGH6 +m4PsZym5Glg+3NfueM9I59ZyF5FPnr1U4WIIGm2aQmm9VQUQKDdXwI70LfduQUn7 +dKCD/Lq3jlm9CRCf9+QyxRLXT33uuI8iCQIDAQABo4GfMIGcMIGZBgNVHREEgZEw +gY6CDmRldi50aW1ldGFiLmlvghFkZXZhcGkudGltZXRhYi5pb4IRZGV2Y2RuLnRp +bWV0YWIuaW+CE3Nob3djYXNlLnRpbWV0YWIuaW+CE2NvdmVyYWdlLnRpbWV0YWIu +aW+CGWRldnVzZXJjb250ZW50LnRpbWV0YWIuaW+CEXN1cnZleS50aW1ldGFiLmlv +MA0GCSqGSIb3DQEBCwUAA4ICAQBnm1aPo3oWe+O2wmHrvgqMn3DBU3qOBIERlxjX +ZhV+uKD6L9Mjm5cDsouKjohMZUfiRJJacx6MqxDIB4tifIC0vj5PDkjhVk1Ga/v4 +8TmQ9p3E49IljymwMVFw+MHqJtuWLtdqkqempT0bXY7lJ1P3GWeQI/eYfitlpotw +E3DW+TKDQSW6uaZoH9IGwDRkDfhp2+ZpCGxzS9pu+MPq9OyMhpruD3YRut58Ougs +J/fuczwfwkaFqFhygcsuF9Ffg5iFn8Inr7PHnnHp/dRhxImFqtb9E8AHTuv/FaPZ +nCCFrNlvaPLqZxdvyZ6QJMx0UeIKp9DQVLnOE+Fjk3AQlns6Uq4iTO3dRXfAoQWj +oQcT6eqKdFveQJvVFQy9JIPtLr07thezTZIMy5t6+WTacBuf1pxBNDLbOPoStlQb +vF0Hp3neLOvyQU48oon+ZlYBImEes11ZH4v1eV8FT00kTHJIRTckXG1UvVRjbhCi +3N1mZIG8BgLLumbzzwzqWJTDWW4HG8hljEWKirojWdaguT8NCpyIx/kHc1Mia2Kw +cd2+RZ+jIXfvxejmiGEjRkx3ODxJByshnKd/ul1j7bn7VAzfYybRDFR8/niMspTe +159yrIbriEIljw+fskCDNMos8WDsJLi9+D0pKf1JdwVujMwoqExZ2qoV3ow39Q04 +swGwUg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +lKu9foMpt1JuW4FoihExTQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +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 +ryiCW30M +-----END CERTIFICATE----- diff --git a/containers/ttio-dev-proxy/certs/privkey.pem b/containers/ttio-dev-proxy/certs/privkey.pem new file mode 100644 index 0000000..89f6220 --- /dev/null +++ b/containers/ttio-dev-proxy/certs/privkey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAviSYeeMQvp1uiK4Yy2DH+FTLKQcxfOtqB07WGs4sbdWPD11Y +jJ//Zln/qUicko/F1dyfh+lyaL69Ww4y5HA6DyGRa94VRnEigRmkbVJlR/KnX1/1 +L0fmf5PL1wngS6shop/UTwqsSc3D3MeFMRXmrnhADoxF2fpDjNxdXRzXda7ChhGZ +p5qUQbE/AKr57HLTuOB4bnVE8ntTauImAy1hGFOQTlf6bmMM+X+tmMmHcBC5dSIa +NGH6m4PsZym5Glg+3NfueM9I59ZyF5FPnr1U4WIIGm2aQmm9VQUQKDdXwI70Lfdu +QUn7dKCD/Lq3jlm9CRCf9+QyxRLXT33uuI8iCQIDAQABAoIBAQC7Z3RExdCOHrp/ +yh+z6+qLzn8CLA3RknFJTKFngAd4JYE/4n/Q8i0WYuPBpEh1h3C0rSMrIKJbrIsT +ALaWQipnGW2rxBJyADXCylQuT4R1WisurHQKqrH60d+ZTSmdSsj28NKfKOTQRRaj +Np8G1xAqq2hvLj/2bFxBrDv09uVBHU+9lWFaCY47FzY3gYz0rwIh4cf3Xb7P3DY5 +3Wol+XVT0BfVAdvIFStzoCT5EHr4cdz10/P8/h71P8NA6PMJQDCmTrQY7dLyho+V +xaPCoCL5Awu5y2AFmnnheomfgpND3Ej06lIR1RNTcQJjX5attaWrbvo2EjIxO16z +kfYMimyBAoGBAN6mb5hOHm2uR/1OZsM8AKuRMVsDUK0FGY1sY0l3uNgAQh8LefOc +YDasgVrueSZQrVoXj25AUhh8GDn307v/47ztfWhSzi1jnEeBnjkHftOUr0Jm8Q6y +IA4ieJXCVBe/ZWygK3FYDxsFXjBx5eOiDZ0MmZEKfYus8M+NaDHo2c7ZAoGBANqf +qkDLx0D08ROyGdYghAMcVmfM/U8KSjwz2cJcnmcDQVscK8t/jSM1fyy0RZtoKJhz +KEQu3mTfrzKTr6+QopvaU7erywJdOf7Yfv5p66eDT+3dmI7vu8T0vmUrpYPSij38 +muILG4XHzyyJxfQUc7ZukLXF8efH/Dyb0yPf2U6xAoGBAL4s12D2SNNSa7cXQns8 +Qy5IZCnjGQPQOVUs4VmdY0tMXS37NX05co0Ap7StwNlTS96KBT8cvYKgbGkHH9mS +5kk3aUi0gdsPHCuPsT/xAQlkFJbZKslsqCiqlOkGBaILH2y7GqBDoRNpcFxczQm0 +H4CnhHv4w+eMHlyJ4hfPVktxAoGATspKLo4CYnukQofmXdBcI07cKQ6soAbCWE4L +hcuhXtjCfhZ6Bh4S/IR0L+VMTMTOFJs0ANavWcVvu5eUMn66y9Z0Y3ZrdI+qrhjS +M9hykG13qe854xGtJz9ZOtbvEMIZBlv6acq9AYrQNGn2yI9yYGYaixgqpXDii+lQ +v9YV2BECgYEAl0yfW0p9826BDZkDZFQXwJQY5a4RYW+ZKI+ukRXn+0VCMTrrpJzw +6AZMnZWKMhacaKAqmOVQAMdTdvCzGMCvMxvCfphDSYLZh+Ftw8rGVSHQdaSn0dDK +XgcrU3lky3WFtZkxS2qsH55tCr835IO29CL/kvhiXLkFKdRUhOl4Wew= +-----END RSA PRIVATE KEY----- diff --git a/containers/ttio-dev-proxy/nginx/conf.d/beta.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/beta.timetab.io.conf new file mode 100644 index 0000000..264a414 --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/beta.timetab.io.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_tokens off; + server_name beta.timetab.io; + + return 301 https://www.timetab.io/beta; +} + +server { + listen 443 ssl http2; + server_name beta.timetab.io; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + return 301 https://www.timetab.io/beta; +} diff --git a/containers/ttio-dev-proxy/nginx/conf.d/coverage.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/coverage.timetab.io.conf new file mode 100644 index 0000000..db622d8 --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/coverage.timetab.io.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_tokens off; + server_name coverage.timetab.io; + + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name coverage.timetab.io; + server_tokens off; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + location /Framework { + index index.html; + try_files $uri $uri/ =404; + alias /var/www/Framework/build/coverage; + } +} diff --git a/containers/ttio-dev-proxy/nginx/conf.d/dev.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/dev.timetab.io.conf new file mode 100644 index 0000000..a580c73 --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/dev.timetab.io.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_tokens off; + server_name dev.timetab.io; + + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name dev.timetab.io; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + root /var/www/Frontend/public; + + add_header content-security-policy "script-src 'self' 'sha256-llPy+U8EGowJjHLQDbvAtCunazzhT1CDW0RRVEI4BgY=' https://www.google-analytics.com/analytics.js; style-src 'self' https://fonts.googleapis.com; img-src 'self' https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://ttio-dev.s3.amazonaws.com; frame-ancestors 'none'; form-action 'self'; block-all-mixed-content; disown-opener; reflected-xss block; referrer no-referrer-when-downgrade;"; + add_header referrer-policy strict-origin-when-cross-origin; + + location / { + try_files $uri @backend; + access_log off; + } + + location @backend { + fastcgi_pass ttio-dev-frontend:9000; + fastcgi_param SCRIPT_FILENAME /data/code/Frontend/index.php; + include fastcgi_params; + } +} diff --git a/containers/ttio-dev-proxy/nginx/conf.d/devapi.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/devapi.timetab.io.conf new file mode 100644 index 0000000..991699a --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/devapi.timetab.io.conf @@ -0,0 +1,13 @@ +server { + listen 443 ssl http2; + server_name devapi.timetab.io; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + location / { + fastcgi_pass ttio-dev-api:9000; + fastcgi_param SCRIPT_FILENAME /data/code/API/index.php; + include fastcgi_params; + } +} diff --git a/containers/ttio-dev-proxy/nginx/conf.d/showcase.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/showcase.timetab.io.conf new file mode 100644 index 0000000..4aee54b --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/showcase.timetab.io.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_tokens off; + server_name showcase.timetab.io; + + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name showcase.timetab.io; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + add_header "Access-Control-Allow-Credentials" true; + add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; + add_header X-Frame-Options "deny"; + add_header X-Content-Type-Options "nosniff"; + add_header X-Xss-Protection "1; mode=block"; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Expires 0; + # add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' fonts.googleapis.com; media-src 'self'; font-src 'self' fonts.gstatic.com; img-src 'self'; frame-ancestors 'none'"; + + root /var/www/Showcase; + + location / { + try_files $uri $uri/ =404; + index index.html; + } +} diff --git a/containers/ttio-dev-proxy/nginx/conf.d/survey.timetab.io.conf b/containers/ttio-dev-proxy/nginx/conf.d/survey.timetab.io.conf new file mode 100644 index 0000000..b0f2154 --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/conf.d/survey.timetab.io.conf @@ -0,0 +1,30 @@ +server { + listen 80; + server_tokens off; + server_name survey.timetab.io; + + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name survey.timetab.io; + + ssl_certificate /data/certs/fullchain.pem; + ssl_certificate_key /data/certs/privkey.pem; + + root /var/www/Frontend/public; + + add_header content-security-policy "script-src 'self' 'sha256-llPy+U8EGowJjHLQDbvAtCunazzhT1CDW0RRVEI4BgY=' https://www.google-analytics.com/analytics.js; style-src 'self' https://fonts.googleapis.com; img-src 'self' https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://ttio-dev.s3.amazonaws.com; frame-ancestors 'none'; form-action 'self'; block-all-mixed-content; disown-opener; reflected-xss block; referrer no-referrer-when-downgrade;"; + + location / { + try_files $uri @backend; + access_log off; + } + + location @backend { + fastcgi_pass ttio-dev-survey:9000; + fastcgi_param SCRIPT_FILENAME /data/code/Survey/index.php; + include fastcgi_params; + } +} diff --git a/containers/ttio-dev-proxy/nginx/nginx.conf b/containers/ttio-dev-proxy/nginx/nginx.conf new file mode 100644 index 0000000..681bfe6 --- /dev/null +++ b/containers/ttio-dev-proxy/nginx/nginx.conf @@ -0,0 +1,38 @@ +user nginx; +worker_processes 1; + +error_log /dev/stderr warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + server_tokens off; + access_log /dev/stdout main; + + keepalive_timeout 65; + + gzip on; + gzip_static on; + gzip_comp_level 9; + gzip_min_length 500; + gzip_types text/plain text/xml text/css application/javascript image/svg+xml image/png; + gzip_vary on; + gzip_http_version 1.1; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/containers/ttio-dev-survey/Dockerfile b/containers/ttio-dev-survey/Dockerfile new file mode 100644 index 0000000..02a8fa5 --- /dev/null +++ b/containers/ttio-dev-survey/Dockerfile @@ -0,0 +1,4 @@ +FROM docker.ttio.cloud:5000/web/survey + +COPY ttio-root-ca.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates diff --git a/containers/ttio-dev-survey/ttio-root-ca.crt b/containers/ttio-dev-survey/ttio-root-ca.crt new file mode 100644 index 0000000..b76b2a6 --- /dev/null +++ b/containers/ttio-dev-survey/ttio-root-ca.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ryiCW30M +-----END CERTIFICATE----- diff --git a/containers/ttio-frontend/Dockerfile b/containers/ttio-frontend/Dockerfile new file mode 100644 index 0000000..20b892b --- /dev/null +++ b/containers/ttio-frontend/Dockerfile @@ -0,0 +1,34 @@ +FROM docker.ttio.cloud:5000/library/fpm + +ARG VERSION +ENV TEMPLATE_FILE /data/code/Frontend/data/templates/template.html + +# Framework +COPY Framework/src /data/code/Framework/src +COPY Framework/lib /data/code/Framework/lib +COPY Framework/bootstrap.php /data/code/Framework/bootstrap.php + +# Library +COPY Library/src /data/code/Library/src +COPY Library/bootstrap.php /data/code/Library/bootstrap.php + +# Ink +COPY Ink/src /data/code/Ink/src + +# Frontend +COPY config/live/frontend.ini /data/code/Frontend/config/system.ini +COPY Frontend/data /data/code/Frontend/data +COPY Frontend/src /data/code/Frontend/src +COPY Frontend/scripts /data/code/Frontend/scripts +COPY Frontend/bootstrap.php /data/code/Frontend/bootstrap.php +COPY Frontend/index.php /data/code/Frontend/index.php + +# Locale +COPY Locale /data/code/Locale +RUN rm /data/code/Locale/Rakefile + +RUN mv ${TEMPLATE_FILE} ${TEMPLATE_FILE}.source && \ + cat ${TEMPLATE_FILE}.source | /data/code/Frontend/scripts/add-versions.sh > ${TEMPLATE_FILE} && \ + rm ${TEMPLATE_FILE}.source + +VOLUME /data/code diff --git a/containers/ttio-postgres/patches/003-survey.sql b/containers/ttio-postgres/patches/003-survey.sql new file mode 100644 index 0000000..c9e1dfe --- /dev/null +++ b/containers/ttio-postgres/patches/003-survey.sql @@ -0,0 +1,23 @@ +CREATE TABLE survey_questions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title VARCHAR(255), + weight INTEGER NOT NULL +); + +CREATE TABLE survey_answers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + value INTEGER NOT NULL DEFAULT 0, + version VARCHAR(255) NOT NULL, + created TIMESTAMP NOT NULL DEFAULT utc_now(), + survey_question_id UUID NOT NULL REFERENCES survey_questions (id), + beta_request_id UUID NOT NULL REFERENCES beta_requests (id) +); + +-- TODO: Populate with survey data +INSERT INTO survey_questions (title, weight) VALUES ('I consider myself to be organized', 2); +INSERT INTO survey_questions (title, weight) VALUES ('I am efficient at looking through my documents and information for a lesson / exam', 1); +INSERT INTO survey_questions (title, weight) VALUES ('I often lose my notes / documents', 2); +INSERT INTO survey_questions (title, weight) VALUES ('I often forget to finish things on time', 1); +INSERT INTO survey_questions (title, weight) VALUES ('I often have trouble knowing where my important documents / files are', 2); + +ALTER TABLE beta_requests ADD survey_before_completed BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/containers/ttio-proxy/Dockerfile b/containers/ttio-proxy/Dockerfile new file mode 100644 index 0000000..a8024ce --- /dev/null +++ b/containers/ttio-proxy/Dockerfile @@ -0,0 +1,19 @@ +FROM library/nginx:alpine + +ARG VERSION + +COPY containers/ttio-proxy/config/nginx/conf.d/* /etc/nginx/conf.d/ +COPY containers/ttio-proxy/config/nginx/* /etc/nginx/ + +COPY Frontend/public/images /var/www/html/images +COPY Frontend/public/favicon.ico /var/www/html/favicon.ico + +COPY Styles/css/application.css /var/www/html/css/application-${VERSION}.css +COPY Styles/icons /var/www/html/icons +COPY Styles/fonts /var/www/html/fonts +COPY Application/build/application.js /var/www/html/js/application-${VERSION}.js +COPY Application/build/polyfills.js /var/www/html/js/polyfills-${VERSION}.js + +VOLUME /data/ssl +VOLUME /data/certs +VOLUME /var/www/letsencrypt diff --git a/containers/ttio-proxy/config/nginx/conf.d/api.timetab.io.conf b/containers/ttio-proxy/config/nginx/conf.d/api.timetab.io.conf new file mode 100644 index 0000000..82a86a3 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/api.timetab.io.conf @@ -0,0 +1,14 @@ +server { + listen 443 ssl http2; + server_name api.timetab.io; + + include ssl_config; + + location / { + fastcgi_pass ttio-api:9000; + fastcgi_param SCRIPT_FILENAME /data/code/API/index.php; + include fastcgi_params; + } +} + + diff --git a/containers/ttio-proxy/config/nginx/conf.d/beta.timetab.io.conf b/containers/ttio-proxy/config/nginx/conf.d/beta.timetab.io.conf new file mode 100644 index 0000000..3d9d453 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/beta.timetab.io.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name beta.timetab.io; + root /var/www/letsencrypt; + + location / { + return 301 https://www.timetab.io/beta; + } + + location '/.well-known/acme-challenge' { + default_type 'text/plain'; + } +} + +server { + listen 443 ssl http2; + server_name beta.timetab.io; + + include ssl_config; + + return 301 https://www.timetab.io/beta; +} diff --git a/containers/ttio-proxy/config/nginx/conf.d/default.conf b/containers/ttio-proxy/config/nginx/conf.d/default.conf new file mode 100644 index 0000000..0b11446 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/default.conf @@ -0,0 +1,13 @@ +server { + listen 80 default_server; + + root /var/www/letsencrypt; + default_type 'text/plain'; + + location / { + return 404 'not found'; + } + + location '/.well-known/acme-challenge' { + } +} diff --git a/containers/ttio-proxy/config/nginx/conf.d/docker.ttio.cloud.conf b/containers/ttio-proxy/config/nginx/conf.d/docker.ttio.cloud.conf new file mode 100644 index 0000000..15067eb --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/docker.ttio.cloud.conf @@ -0,0 +1,11 @@ +server { + listen 443 ssl http2; + server_name docker.ttio.cloud; + + ssl_certificate /data/certs/live/docker.ttio.cloud/fullchain.pem; + ssl_certificate_key /data/certs/live/docker.ttio.cloud/privkey.pem; + + location / { + proxy_pass https://docker.ttio.cloud:5000; + } +} diff --git a/containers/ttio-proxy/config/nginx/conf.d/survey.timetab.io.conf b/containers/ttio-proxy/config/nginx/conf.d/survey.timetab.io.conf new file mode 100644 index 0000000..aa211d5 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/survey.timetab.io.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name survey.timetab.io; + root /var/www/letsencrypt; + + location / { + return 301 https://$server_name$request_uri; + } + + location '/.well-known/acme-challenge' { + default_type 'text/plain'; + } +} + +server { + listen 443 ssl http2; + server_name survey.timetab.io; + + root /var/www/html; + + include ssl_config; + + add_header content-security-policy "script-src 'self' 'sha256-llPy+U8EGowJjHLQDbvAtCunazzhT1CDW0RRVEI4BgY=' https://www.google-analytics.com/analytics.js; style-src 'self' https://fonts.googleapis.com; img-src 'self' https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://ttio.s3.amazonaws.com; frame-ancestors 'none'; form-action 'self'; block-all-mixed-content; disown-opener; reflected-xss block; referrer no-referrer-when-downgrade;"; + + location / { + try_files $uri @backend; + add_header cache-control "public, max-age=2592000"; + access_log off; + } + + location @backend { + access_log /var/log/nginx/access.log; + fastcgi_pass ttio-survey:9000; + fastcgi_param SCRIPT_FILENAME /data/code/Survey/index.php; + include fastcgi_params; + } +} + + diff --git a/containers/ttio-proxy/config/nginx/conf.d/timetab.io.conf b/containers/ttio-proxy/config/nginx/conf.d/timetab.io.conf new file mode 100644 index 0000000..181b078 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/conf.d/timetab.io.conf @@ -0,0 +1,48 @@ +server { + listen 80; + server_name www.timetab.io timetab.io; + root /var/www/letsencrypt; + + location / { + return 301 https://$server_name$request_uri; + } + + location '/.well-known/acme-challenge' { + default_type 'text/plain'; + } +} + +server { + listen 443 ssl http2; + server_name timetab.io; + + include ssl_config; + + return 301 https://www.$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name www.timetab.io; + + root /var/www/html; + + include ssl_config; + + add_header content-security-policy "script-src 'self' 'sha256-llPy+U8EGowJjHLQDbvAtCunazzhT1CDW0RRVEI4BgY=' https://www.google-analytics.com/analytics.js; style-src 'self' https://fonts.googleapis.com; img-src 'self' https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://ttio.s3.amazonaws.com; frame-ancestors 'none'; form-action 'self'; block-all-mixed-content; disown-opener; reflected-xss block; referrer no-referrer-when-downgrade;"; + + location / { + try_files $uri @backend; + add_header cache-control "public, max-age=2592000"; + access_log off; + } + + location @backend { + access_log /var/log/nginx/access.log; + fastcgi_pass ttio-frontend:9000; + fastcgi_param SCRIPT_FILENAME /data/code/Frontend/index.php; + include fastcgi_params; + } +} + + diff --git a/containers/ttio-proxy/config/nginx/nginx.conf b/containers/ttio-proxy/config/nginx/nginx.conf new file mode 100644 index 0000000..2402f77 --- /dev/null +++ b/containers/ttio-proxy/config/nginx/nginx.conf @@ -0,0 +1,37 @@ +user nginx; +worker_processes 1; + +error_log /dev/stderr warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + gzip on; + gzip_static on; + gzip_comp_level 9; + gzip_min_length 500; + gzip_types text/plain text/xml text/css application/javascript image/svg+xml image/png application/json; + gzip_vary on; + gzip_http_version 1.1; + + server_tokens off; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/containers/ttio-proxy/config/nginx/ssl_config b/containers/ttio-proxy/config/nginx/ssl_config new file mode 100644 index 0000000..bc40abc --- /dev/null +++ b/containers/ttio-proxy/config/nginx/ssl_config @@ -0,0 +1,17 @@ +ssl_certificate /data/certs/live/www.timetab.io/fullchain.pem; +ssl_certificate_key /data/certs/live/www.timetab.io/privkey.pem; + +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + +ssl_prefer_server_ciphers on; +ssl_session_cache shared:SSL:10m; + +ssl_dhparam /data/ssl/dhparams.pem; + +# force browser to use https for 180 days +add_header strict-transport-security max-age=15552000; +add_header x-frame-options deny; +add_header x-xss-protection '1; mode=block'; +add_header x-content-type-options nosniff; +add_header referrer-policy strict-origin-when-cross-origin; diff --git a/containers/ttio-staging/Dockerfile b/containers/ttio-staging/Dockerfile new file mode 100644 index 0000000..f616dbe --- /dev/null +++ b/containers/ttio-staging/Dockerfile @@ -0,0 +1,15 @@ +FROM library/centos:7 + +COPY config/docker.repo /etc/yum.repos.d/docker.repo + +RUN yum -y install openssl systemd docker-engine && \ + yum clean all + +RUN systemctl enable docker + +EXPOSE 80 +EXPOSE 443 + +VOLUME /etc/letsencrypt/live + +CMD /usr/sbin/init diff --git a/containers/ttio-staging/config/docker.repo b/containers/ttio-staging/config/docker.repo new file mode 100644 index 0000000..9c4eeed --- /dev/null +++ b/containers/ttio-staging/config/docker.repo @@ -0,0 +1,6 @@ +[dockerrepo] +name=Docker Repository +baseurl=https://yum.dockerproject.org/repo/main/centos/7/ +enabled=1 +gpgcheck=1 +gpgkey=https://yum.dockerproject.org/gpg diff --git a/containers/ttio-survey/Dockerfile b/containers/ttio-survey/Dockerfile new file mode 100644 index 0000000..88d02fd --- /dev/null +++ b/containers/ttio-survey/Dockerfile @@ -0,0 +1,45 @@ +FROM docker.ttio.cloud:5000/library/fpm + +RUN apt-get update && \ + apt-get -y install locales && \ + echo "de_CH UTF-8" >> /etc/locale.gen && \ + echo "en_GB UTF-8" >> /etc/locale.gen && \ + locale-gen && \ + apt-get clean + +ARG VERSION +ENV TEMPLATE_FILE /data/code/Survey/data/templates/template.html + +# Framework +COPY Framework/src /data/code/Framework/src +COPY Framework/lib /data/code/Framework/lib +COPY Framework/bootstrap.php /data/code/Framework/bootstrap.php + +# Library +COPY Library/src /data/code/Library/src +COPY Library/bootstrap.php /data/code/Library/bootstrap.php + +# Ink +COPY Ink/src /data/code/Ink/src + +# Survey +COPY config/live/survey.ini /data/code/Survey/config/system.ini +COPY Survey/data /data/code/Survey/data +COPY Survey/src /data/code/Survey/src +COPY Survey/bootstrap.php /data/code/Survey/bootstrap.php +COPY Survey/index.php /data/code/Survey/index.php + +# Frontend +COPY Frontend/scripts/add-versions.sh /data/code/Frontend/scripts/add-versions.sh +COPY Frontend/src /data/code/Frontend/src +COPY Frontend/bootstrap.php /data/code/Frontend/bootstrap.php + +# Locale +COPY Locale /data/code/Locale +RUN rm /data/code/Locale/Rakefile + +RUN mv ${TEMPLATE_FILE} ${TEMPLATE_FILE}.source && \ + cat ${TEMPLATE_FILE}.source | /data/code/Frontend/scripts/add-versions.sh > ${TEMPLATE_FILE} && \ + rm ${TEMPLATE_FILE}.source + +VOLUME /data/code diff --git a/containers/ttio-worker/Dockerfile b/containers/ttio-worker/Dockerfile new file mode 100644 index 0000000..03438dd --- /dev/null +++ b/containers/ttio-worker/Dockerfile @@ -0,0 +1,29 @@ +FROM docker.ttio.cloud:5000/library/php + +# Framework +COPY Framework/src /data/code/Framework/src +COPY Framework/lib /data/code/Framework/lib +COPY Framework/bootstrap.php /data/code/Framework/bootstrap.php + +# Library +COPY Library/src /data/code/Library/src +COPY Library/bootstrap.php /data/code/Library/bootstrap.php + +# Ink +COPY Ink/src /data/code/Ink/src + +# Worker +COPY config/live/worker.ini /data/code/Worker/config/system.ini +COPY Worker/data /data/code/Worker/data +COPY Worker/src /data/code/Worker/src +COPY Worker/bootstrap.php /data/code/Worker/bootstrap.php +COPY Worker/worker.php /data/code/Worker/worker.php +COPY Worker/push.php /data/code/Worker/push.php + +# Locale +COPY Locale /data/code/Locale +RUN rm /data/code/Locale/Rakefile + +VOLUME /data/code + +CMD ["/data/code/Worker/worker.php"] diff --git a/data/elastic-mappings.json b/data/elastic-mappings.json new file mode 100644 index 0000000..4c735ba --- /dev/null +++ b/data/elastic-mappings.json @@ -0,0 +1,127 @@ +{ + "settings": { + "analysis": { + "analyzer": { + "ttio_text": { + "type": "custom", + "tokenizer": "standard", + "filter": [ + "standard", + "lowercase", + "ttio_ascii_folding", + "ttio_word_delimiter" + ] + }, + "ttio_ngram": { + "type": "custom", + "tokenizer": "standard", + "filter": [ + "standard", + "lowercase", + "ttio_ascii_folding", + "ttio_word_delimiter", + "ttio_ngram" + ] + } + }, + "filter": { + "ttio_ascii_folding": { + "type": "asciifolding", + "preserve_original": true + }, + "ttio_word_delimiter": { + "type": "word_delimiter", + "preserve_original": true, + "stem_english_possessive": true + }, + "ttio_ngram": { + "type": "edgeNGram", + "min_gram": 1, + "max_gram": 6, + "preserve_original": true + } + } + } + }, + "mappings": { + "feed": { + "_all": { + "enabled": false + }, + "properties": { + "name": { + "type": "text", + "analyzer": "ttio_text", + "fields": { + "ngram": { + "type": "text", + "analyzer": "ttio_ngram" + } + } + }, + "description": { + "type": "text", + "analyzer": "ttio_text", + "fields": { + "ngram": { + "type": "text", + "analyzer": "ttio_ngram" + } + } + }, + "created": { + "type": "date", + "format": "epoch_second" + }, + "updated": { + "type": "date", + "format": "epoch_second" + }, + "is_private": { + "type": "boolean" + }, + "_feed_id": { + "type": "keyword" + } + } + }, + "post": { + "_all": { + "enabled": false + }, + "properties": { + "title": { + "type": "text", + "analyzer": "ttio_text", + "fields": { + "ngram": { + "type": "text", + "analyzer": "ttio_ngram" + } + } + }, + "body": { + "type": "text", + "analyzer": "ttio_text", + "fields": { + "ngram": { + "type": "text", + "analyzer": "ttio_ngram" + } + } + }, + "created": { + "type": "date", + "format": "epoch_second" + }, + "updated": { + "type": "date", + "format": "epoch_second" + }, + "_feed_id": { + "type": "keyword" + } + } + } + } +} diff --git a/data/patches/001-feed-vanity-update.sql b/data/patches/001-feed-vanity-update.sql new file mode 100644 index 0000000..6889363 --- /dev/null +++ b/data/patches/001-feed-vanity-update.sql @@ -0,0 +1 @@ +CREATE TRIGGER update_feed_vanities_timestamp_column BEFORE UPDATE ON feed_vanities FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); diff --git a/data/patches/002-feed-description.sql b/data/patches/002-feed-description.sql new file mode 100644 index 0000000..6555dda --- /dev/null +++ b/data/patches/002-feed-description.sql @@ -0,0 +1,20 @@ +ALTER TABLE feeds ADD description VARCHAR(255) NOT NULL DEFAULT ''; + +DROP VIEW aggregated_feeds; +CREATE VIEW aggregated_feeds AS + SELECT feeds.*, + users.id AS owner_id, + users.username AS owner_username, + users.name AS owner_name + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id AND is_owner(feed_users.role) + JOIN users + ON feed_users.user_id = users.id; + +DROP VIEW user_feeds; +CREATE VIEW user_feeds AS + SELECT feeds.*, feed_users.user_id + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id; \ No newline at end of file diff --git a/data/schema.sql b/data/schema.sql new file mode 100644 index 0000000..ff4c7fc --- /dev/null +++ b/data/schema.sql @@ -0,0 +1,224 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE OR REPLACE FUNCTION update_timestamp_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated = utc_now(); + RETURN NEW; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE OR REPLACE FUNCTION utc_now() +RETURNS TIMESTAMP as $$ +BEGIN + RETURN now() AT TIME ZONE 'UTC'; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + password VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL DEFAULT '', + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE UNIQUE INDEX users_lower_username ON users (lower(username)); + +CREATE TRIGGER update_users_timestamp_column BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); + +CREATE TABLE IF NOT EXISTS verification_tokens ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + token VARCHAR(255) NOT NULL, + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS feeds ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL DEFAULT '', + description VARCHAR(255) NOT NULL DEFAULT '', + is_private BOOLEAN NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + owner_id UUID REFERENCES users (id), + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE TRIGGER update_feeds_timestamp_column BEFORE UPDATE ON feeds FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); +CREATE INDEX IF NOT EXISTS feeds_owner_id ON feeds (owner_id); + +CREATE TABLE feed_vanities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + feed_id UUID NOT NULL UNIQUE REFERENCES feeds (id) +); + +CREATE UNIQUE INDEX feed_vanities_name ON feed_vanities (lower(name)); +CREATE TRIGGER update_feed_vanities_timestamp_column BEFORE UPDATE ON feed_vanities FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); + +CREATE VIEW public_feeds AS + SELECT feeds.id, + feeds.name, + feeds.is_verified, + feeds.created, + feeds.updated, + users.id AS owner_id, + users.name AS owner_name, + users.username AS owner_username + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id AND is_owner(feed_users.role) + JOIN users + ON feed_users.user_id = users.id + WHERE is_private = FALSE; + +CREATE TYPE post_type AS ENUM ('note', 'task', 'event'); + +CREATE TABLE IF NOT EXISTS posts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + feed_id UUID NOT NULL REFERENCES feeds (id) ON DELETE CASCADE, + author_id UUID NOT NULL REFERENCES users (id), + type post_type DEFAULT 'note', + title VARCHAR(255) NOT NULL DEFAULT '', + body TEXT NOT NULL DEFAULT '', + timestamp TIMESTAMP, + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE INDEX IF NOT EXISTS posts_feed_id ON posts (feed_id); +CREATE INDEX IF NOT EXISTS posts_timestamp ON posts (timestamp); +CREATE INDEX IF NOT EXISTS posts_type ON posts (type); +CREATE INDEX IF NOT EXISTS posts_type_timestamp ON posts (type, timestamp ASC); + +CREATE VIEW aggregated_posts AS + SELECT posts.*, + users.username as author_username, + users.name as author_name, + feeds.name as feed_name + FROM posts + JOIN users ON posts.author_id = users.id + JOIN feeds ON posts.feed_id = feeds.id; + +-- TODO: define sorting (NOTE: timestamp might be null, so consider that as well) +CREATE VIEW uncompleted_tasks AS + SELECT posts.*, feed_users.user_id + FROM posts + JOIN feed_users + ON feed_users.feed_id = posts.feed_id + LEFT OUTER JOIN post_annotations AS meta + ON meta.post_id = posts.id + WHERE posts.type = 'task' + AND meta.is_checked IS NOT TRUE; + +CREATE VIEW upcoming_events AS + SELECT posts.*, feed_users.user_id FROM posts + JOIN feed_users + ON feed_users.feed_id = posts.feed_id + WHERE posts.type = 'event' + AND posts.timestamp > utc_now() + ORDER BY posts.timestamp ASC; + +CREATE TRIGGER update_posts_timestamp_column BEFORE UPDATE ON posts FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); + +CREATE TABLE IF NOT EXISTS post_annotations ( + post_id UUID NOT NULL REFERENCES posts (id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + is_checked BOOLEAN DEFAULT FALSE, + PRIMARY KEY (post_id, user_id) +); + +CREATE INDEX IF NOT EXISTS post_annotations_is_checked ON post_annotations (is_checked); + +CREATE TABLE files ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + public_id VARCHAR(255) NOT NULL UNIQUE, + owner_id UUID NOT NULL REFERENCES users (id), + name VARCHAR(255), + mime_type VARCHAR(255), + created TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE TABLE post_attachments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + post_id UUID NOT NULL REFERENCES posts (id) ON DELETE CASCADE, + file_id UUID NOT NULL REFERENCES files (id) ON DELETE CASCADE, + created TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE TYPE feed_user_role AS ENUM ('default', 'moderator', 'owner'); +CREATE OR REPLACE FUNCTION is_owner(role feed_user_role) +RETURNS BOOLEAN AS +$$ +BEGIN + RETURN role >= 'owner'::feed_user_role; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE TABLE IF NOT EXISTS feed_users ( + feed_id UUID NOT NULL REFERENCES feeds (id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + role feed_user_role NOT NULL DEFAULT 'default'::feed_user_role, + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now(), + PRIMARY KEY(feed_id, user_id) +); + +CREATE INDEX feed_users_owner ON feed_users (feed_id, is_owner(role)); + +CREATE TRIGGER update_feed_users_timestamp_column BEFORE UPDATE ON feed_users FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); + +CREATE VIEW aggregated_feeds AS + SELECT feeds.*, + users.id AS owner_id, + users.username AS owner_username, + users.name AS owner_name + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id AND is_owner(feed_users.role) + JOIN users + ON feed_users.user_id = users.id; + +CREATE VIEW user_feeds AS + SELECT feeds.*, feed_users.user_id + FROM feeds + JOIN feed_users + ON feeds.id = feed_users.feed_id; + +CREATE TABLE IF NOT EXISTS collections ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL DEFAULT '', + owner_id UUID NOT NULL REFERENCES users (id), + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now() +); + +CREATE TRIGGER update_collections_timestamp_column BEFORE UPDATE ON collections FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); +CREATE INDEX collections_owner_id ON collections (owner_id); + +CREATE TABLE IF NOT EXISTS collection_posts ( + collection_id UUID NOT NULL REFERENCES collections (id), + post_id UUID NOT NULL REFERENCES posts (id), + PRIMARY KEY (collection_id, post_id) +); + +CREATE TABLE beta_requests ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + created TIMESTAMP NOT NULL DEFAULT utc_now(), + approved BOOLEAN NOT NULL DEFAULT FALSE, + email VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE feed_invitations ( + feed_id UUID NOT NULL REFERENCES feeds (id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, + role feed_user_role NOT NULL DEFAULT 'default'::feed_user_role, + created TIMESTAMP NOT NULL DEFAULT utc_now(), + updated TIMESTAMP NOT NULL DEFAULT utc_now(), + PRIMARY KEY(feed_id, user_id) +); + +CREATE TRIGGER update_feed_invitations_timestamp BEFORE UPDATE ON feed_invitations FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column(); diff --git a/packages/ttio-docker-registry/auth/htpasswd b/packages/ttio-docker-registry/auth/htpasswd new file mode 100644 index 0000000..604afc9 --- /dev/null +++ b/packages/ttio-docker-registry/auth/htpasswd @@ -0,0 +1,3 @@ +bot:$2y$05$IuHTmIKRsYjd98Yaha/Ys.LX8IdsmXIDJJ7FWz2Ko7s5E9mPZbY7i +bash:$2y$05$h6kPlgzZ.DwUD7TrvK8Xae3LXTZi5KuNL/eh7OvLIg5WWQBz7KjH2 +thatdeveloper:$2y$05$EPS1phvaNofIsEQTrsS4z.CIyg3Dq5W4b4mSQpG/02sNNZDtv8ctC diff --git a/packages/ttio-docker-registry/package.spec b/packages/ttio-docker-registry/package.spec new file mode 100644 index 0000000..05fc27d --- /dev/null +++ b/packages/ttio-docker-registry/package.spec @@ -0,0 +1,42 @@ +Name: ttio-docker-registry +Version: 0.0.2 +Release: ttio.3 +Summary: docker registry +License: All rights reserved +BuildArch: noarch +Requires: docker-engine + +%description +docker registry + +%build +rm -rf ${RPM_BUILD_DIR}/* + +cp -R %{_packagedir}/auth ${RPM_BUILD_DIR}/auth +cp -R %{_packagedir}/units ${RPM_BUILD_DIR}/units + +%install +install -m 755 -d ${RPM_BUILD_ROOT}/etc/ttio/docker-registry/auth +install -m 755 -d ${RPM_BUILD_ROOT}/etc/ttio/docker-registry/data + +install -m 755 -d ${RPM_BUILD_ROOT}/etc/systemd/system + +cp ${RPM_BUILD_DIR}/auth/* ${RPM_BUILD_ROOT}/etc/ttio/docker-registry/auth/ +cp ${RPM_BUILD_DIR}/units/* ${RPM_BUILD_ROOT}/etc/systemd/system/ + +%post +systemctl daemon-reload +systemctl restart ttio-docker-registry.service + +%clean +rm -rf ${RPM_BUILD_ROOT} +rm -rf ${RPM_BUILD_DIR} + +%files +%defattr(-,root,root) + +%dir /etc/systemd/system +%dir /etc/ttio/docker-registry + +/etc/ttio/docker-registry/* +/etc/systemd/system/* diff --git a/packages/ttio-docker-registry/units/ttio-docker-registry.service b/packages/ttio-docker-registry/units/ttio-docker-registry.service new file mode 100644 index 0000000..da78a2f --- /dev/null +++ b/packages/ttio-docker-registry/units/ttio-docker-registry.service @@ -0,0 +1,25 @@ +[Unit] +Description=timetab.io Docker Registry +Requires=docker.service +After=docker.service + +[Service] +Restart=always +ExecStart=/usr/bin/docker run \ + -a stdin -a stdout -a stderr \ + -p 5000:5000 \ + -v /etc/ttio/docker-registry/auth:/auth \ + -v /etc/letsencrypt:/certs \ + -v /etc/ttio/docker-registry/data:/var/lib/registry \ + --name ttio-docker-registry \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM=ttio-docker-registry \ + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ + -e REGISTRY_HTTP_HOST=https://docker.ttio.cloud:5000 \ + -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/live/docker.ttio.cloud/fullchain.pem \ + -e REGISTRY_HTTP_TLS_KEY=/certs/live/docker.ttio.cloud/privkey.pem \ + registry:2 +ExecStopPost=/usr/bin/docker rm -f ttio-docker-registry + +[Install] +WantedBy=default.target diff --git a/packages/ttio-server/config/letsencrypt/docker.ttio.cloud.ini b/packages/ttio-server/config/letsencrypt/docker.ttio.cloud.ini new file mode 100644 index 0000000..52aaff5 --- /dev/null +++ b/packages/ttio-server/config/letsencrypt/docker.ttio.cloud.ini @@ -0,0 +1,7 @@ +rsa-key-size = 4096 +email = ruben.schmidmeister@icloud.com +domains = docker.ttio.cloud +text = True +authenticator = webroot +webroot-path = /data/web +renew-by-default = True diff --git a/packages/ttio-server/config/letsencrypt/timetab.io.ini b/packages/ttio-server/config/letsencrypt/timetab.io.ini new file mode 100644 index 0000000..dfd2519 --- /dev/null +++ b/packages/ttio-server/config/letsencrypt/timetab.io.ini @@ -0,0 +1,7 @@ +rsa-key-size = 4096 +email = ruben.schmidmeister@icloud.com +domains = www.timetab.io, timetab.io, api.timetab.io, cdn.timetab.io, beta.timetab.io, survey.timetab.io +text = True +authenticator = webroot +webroot-path = /data/web +renew-by-default = True diff --git a/packages/ttio-server/config/renew-certs.sh b/packages/ttio-server/config/renew-certs.sh new file mode 100755 index 0000000..0113fa1 --- /dev/null +++ b/packages/ttio-server/config/renew-certs.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +CONFIGS=/etc/letsencrypt/*.ini + +for CONFIG in ${CONFIGS[@]}; do + /usr/bin/docker run -it --rm --name certbot \ + -v /var/www/letsencrypt:/data/web \ + -v /etc/letsencrypt:/etc/letsencrypt \ + -v /var/lib/letsencrypt:/var/lib/letsencrypt \ + quay.io/letsencrypt/letsencrypt:latest certonly --config ${CONFIG} +done + +/usr/bin/systemctl restart ttio-proxy + diff --git a/packages/ttio-server/cron.d/renew-certs b/packages/ttio-server/cron.d/renew-certs new file mode 100644 index 0000000..4140460 --- /dev/null +++ b/packages/ttio-server/cron.d/renew-certs @@ -0,0 +1 @@ +0 0 1 * * root /usr/local/bin/renew-certs diff --git a/packages/ttio-server/package.spec b/packages/ttio-server/package.spec new file mode 100644 index 0000000..abcebf9 --- /dev/null +++ b/packages/ttio-server/package.spec @@ -0,0 +1,41 @@ +%define version %(echo $VERSION) + +Name: ttio-server +Version: %{version} +Release: ttio.1 +Summary: timetab.io server +License: All rights reserved +BuildArch: noarch +Requires: docker-engine + +%description +timetab.io server + +%build +rm -rf ${RPM_BUILD_DIR}/* + +cp -R %{_packagedir}/config/letsencrypt ${RPM_BUILD_DIR}/letsencrypt +cp -R %{_packagedir}/cron.d ${RPM_BUILD_DIR}/cron.d +cp %{_packagedir}/config/renew-certs.sh ${RPM_BUILD_DIR}/renew-certs.sh + +%install +install -m 755 -d ${RPM_BUILD_ROOT}/etc/letsencrypt +install -m 755 -d ${RPM_BUILD_ROOT}/etc/cron.d +install -m 755 -d ${RPM_BUILD_ROOT}/usr/local/bin + +cp ${RPM_BUILD_DIR}/letsencrypt/* ${RPM_BUILD_ROOT}/etc/letsencrypt/ +cp ${RPM_BUILD_DIR}/cron.d/* ${RPM_BUILD_ROOT}/etc/cron.d/ +cp ${RPM_BUILD_DIR}/renew-certs.sh ${RPM_BUILD_ROOT}/usr/local/bin/renew-certs + +%clean +rm -rf ${RPM_BUILD_ROOT} +rm -rf ${RPM_BUILD_DIR} + +%files +%defattr(-,root,root) +%dir /etc/letsencrypt +%dir /etc/cron.d +%dir /usr/local/bin +/etc/letsencrypt/* +/etc/cron.d/* +/usr/local/bin/* diff --git a/packages/ttio-web/cron.d/run-tasks b/packages/ttio-web/cron.d/run-tasks new file mode 100644 index 0000000..8f737e9 --- /dev/null +++ b/packages/ttio-web/cron.d/run-tasks @@ -0,0 +1 @@ +*/5 * * * * root /usr/bin/docker run --net ttio-net --rm -i docker.ttio.cloud:5000/web/worker:current /data/code/Worker/push.php DeleteUnusedFiles diff --git a/packages/ttio-web/package.spec b/packages/ttio-web/package.spec new file mode 100644 index 0000000..9117bbb --- /dev/null +++ b/packages/ttio-web/package.spec @@ -0,0 +1,99 @@ +%define version %(echo $VERSION) + +Name: ttio-web +Version: %{version} +Release: ttio.1 +Summary: timetab.io web +License: All rights reserved +BuildArch: noarch +Requires: docker-engine, openssl + +%description +timetab.io web + +%build +rm -rf ${RPM_BUILD_DIR}/* + +cp -R %{_packagedir}/units ${RPM_BUILD_DIR}/units +cp -R ${RPM_SOURCE_DIR}/API/scripts ${RPM_BUILD_DIR}/scripts +cp -R ${RPM_SOURCE_DIR}/data ${RPM_BUILD_DIR}/data +cp -R %{_packagedir}/cron.d ${RPM_BUILD_DIR}/cron.d + +%install +install -m 755 -d ${RPM_BUILD_ROOT}/etc/systemd/system +install -m 755 -d ${RPM_BUILD_ROOT}/etc/cron.d +install -m 755 -d ${RPM_BUILD_ROOT}/data/patches + +cp ${RPM_BUILD_DIR}/units/* ${RPM_BUILD_ROOT}/etc/systemd/system/ +cp ${RPM_BUILD_DIR}/cron.d/* ${RPM_BUILD_ROOT}/etc/cron.d/ +cp ${RPM_BUILD_DIR}/data/patches/*.sql ${RPM_BUILD_ROOT}/data/patches/ + +%clean +rm -rf ${RPM_BUILD_ROOT} +rm -rf ${RPM_BUILD_DIR} + +%post +mkdir -p /data/redis +mkdir -p /data/nsalog +mkdir -p /data/ssl +mkdir -p /var/www/letsencrypt + +if [ -z $(docker network inspect --format '{{.Name}}' ttio-net 2> /dev/null) ]; then + echo "Creating network" + docker network create ttio-net +fi + +if [ ! -f /data/ssl/dhparams.pem ]; then + openssl dhparam -out /data/ssl/dhparams.pem 2048 +fi + +docker pull docker.ttio.cloud:5000/web/proxy:%{version} +docker tag docker.ttio.cloud:5000/web/proxy:%{version} docker.ttio.cloud:5000/web/proxy:current + +docker pull docker.ttio.cloud:5000/web/api:%{version} +docker tag docker.ttio.cloud:5000/web/api:%{version} docker.ttio.cloud:5000/web/api:current + +docker pull docker.ttio.cloud:5000/web/frontend:%{version} +docker tag docker.ttio.cloud:5000/web/frontend:%{version} docker.ttio.cloud:5000/web/frontend:current + +docker pull docker.ttio.cloud:5000/web/survey:%{version} +docker tag docker.ttio.cloud:5000/web/survey:%{version} docker.ttio.cloud:5000/web/survey:current + +docker pull docker.ttio.cloud:5000/web/worker:%{version} +docker tag docker.ttio.cloud:5000/web/worker:%{version} docker.ttio.cloud:5000/web/worker:current + +docker pull docker.ttio.cloud:5000/library/redis:latest +docker tag docker.ttio.cloud:5000/library/redis:latest docker.ttio.cloud:5000/library/redis:current + +docker pull docker.ttio.cloud:5000/library/postgres:latest +docker tag docker.ttio.cloud:5000/library/postgres:latest docker.ttio.cloud:5000/library/postgres:current + +docker pull docker.ttio.cloud:5000/library/elastic:latest +docker tag docker.ttio.cloud:5000/library/elastic:latest docker.ttio.cloud:5000/library/elastic:current + +systemctl daemon-reload + +systemctl enable ttio-web.target +systemctl restart ttio-web.target + +docker exec -i ttio-api /data/code/API/scripts/create-system-token.php + +docker run --rm \ + --net ttio-net \ + -v /data/patches:/data/patches \ + -v /data/applied-patches:/data/applied \ + docker.ttio.cloud:5000/library/postgres \ + env TERM=xterm POSTGRES_HOST=ttio-postgres ttio-patch + +docker run --rm \ + --net ttio-net \ + docker.ttio.cloud:5000/web/worker:current /data/code/Worker/push.php Initial + +%files +%defattr(-,root,root) +%dir /etc/systemd/system +%dir /etc/cron.d +%dir /data/patches +/etc/systemd/system/* +/etc/cron.d/* +/data/patches/* diff --git a/packages/ttio-web/units/ttio-api.service b/packages/ttio-web/units/ttio-api.service new file mode 100644 index 0000000..974ac85 --- /dev/null +++ b/packages/ttio-web/units/ttio-api.service @@ -0,0 +1,18 @@ +[Unit] +Description=timetab.io API +After=ttio-redis.service ttio-postgres.service docker.service +Requires=ttio-redis.service ttio-postgres.service ttio-elastic.service docker.service +BindsTo=ttio-web.target + +[Service] +Restart=always +ExecStartPre=/usr/bin/touch /data/nsalog/api.txt +ExecStartPre=-/usr/bin/docker rm -f ttio-api +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-api \ + --net ttio-net \ + -v /data/nsalog/api.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/api:current +ExecStart=/usr/bin/docker logs -f ttio-api +ExecStop=/usr/bin/docker stop ttio-api +ExecStopPost=/usr/bin/docker rm -f ttio-api diff --git a/packages/ttio-web/units/ttio-elastic.service b/packages/ttio-web/units/ttio-elastic.service new file mode 100644 index 0000000..e1cd3d0 --- /dev/null +++ b/packages/ttio-web/units/ttio-elastic.service @@ -0,0 +1,16 @@ +[Unit] +Description=timetab.io Elasticsearch +After=docker.service +Requires=docker.service + +[Service] +Restart=always +ExecStartPre=-/usr/bin/docker rm -f ttio-elastic +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-elastic \ + --net ttio-net \ + -v /data/elastic:/usr/share/elasticsearch/data \ + docker.ttio.cloud:5000/library/elastic:current +ExecStart=/usr/bin/docker logs -f ttio-elastic +ExecStop=/usr/bin/docker stop ttio-elastic +ExecStopPost=/usr/bin/docker rm -f ttio-elastic diff --git a/packages/ttio-web/units/ttio-frontend.service b/packages/ttio-web/units/ttio-frontend.service new file mode 100644 index 0000000..d7292e0 --- /dev/null +++ b/packages/ttio-web/units/ttio-frontend.service @@ -0,0 +1,18 @@ +[Unit] +Description=timetab.io Frontend +After=ttio-redis.service ttio-api.service docker.service +Requires=ttio-redis.service ttio-api.service docker.service +BindsTo=ttio-web.target + +[Service] +Restart=always +ExecStartPre=/usr/bin/touch /data/nsalog/frontend.txt +ExecStartPre=-/usr/bin/docker rm -f ttio-frontend +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-frontend \ + --net ttio-net \ + -v /data/nsalog/frontend.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/frontend:current +ExecStart=/usr/bin/docker logs -f ttio-frontend +ExecStop=/usr/bin/docker stop ttio-frontend +ExecStopPost=/usr/bin/docker rm -f ttio-frontend diff --git a/packages/ttio-web/units/ttio-postgres.service b/packages/ttio-web/units/ttio-postgres.service new file mode 100644 index 0000000..45029be --- /dev/null +++ b/packages/ttio-web/units/ttio-postgres.service @@ -0,0 +1,16 @@ +[Unit] +Description=timetab.io Postgres +After=docker.service +Requires=docker.service + +[Service] +Restart=always +ExecStartPre=-/usr/bin/docker rm -f ttio-postgres +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-postgres \ + --net ttio-net \ + -v /data/postgres:/var/lib/postgresql/data \ + docker.ttio.cloud:5000/library/postgres:current +ExecStart=/usr/bin/docker logs -f ttio-postgres +ExecStop=/usr/bin/docker stop ttio-postgres +ExecStopPost=/usr/bin/docker rm -f ttio-postgres diff --git a/packages/ttio-web/units/ttio-proxy.service b/packages/ttio-web/units/ttio-proxy.service new file mode 100644 index 0000000..b552b85 --- /dev/null +++ b/packages/ttio-web/units/ttio-proxy.service @@ -0,0 +1,21 @@ +[Unit] +Description=timetab.io Proxy +After=ttio-frontend.service ttio-api.service docker.service +Requires=ttio-frontend.service ttio-api.service docker.service +BindsTo=ttio-web.target + +[Service] +Restart=always +ExecStartPre=-/usr/bin/docker rm -f ttio-proxy +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-proxy \ + --net ttio-net \ + -p 80:80 \ + -p 443:443 \ + -v /var/www/letsencrypt:/var/www/letsencrypt \ + -v /etc/letsencrypt:/data/certs \ + -v /data/ssl:/data/ssl \ + docker.ttio.cloud:5000/web/proxy:current +ExecStart=/usr/bin/docker logs -f ttio-proxy +ExecStop=/usr/bin/docker stop ttio-proxy +ExecStopPost=/usr/bin/docker rm -f ttio-proxy diff --git a/packages/ttio-web/units/ttio-redis.service b/packages/ttio-web/units/ttio-redis.service new file mode 100644 index 0000000..9327003 --- /dev/null +++ b/packages/ttio-web/units/ttio-redis.service @@ -0,0 +1,16 @@ +[Unit] +Description=timetab.io Redis +After=docker.service +Requires=docker.service + +[Service] +Restart=always +ExecStartPre=-/usr/bin/docker rm -f ttio-redis +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-redis \ + --net ttio-net \ + -v /data/redis:/data \ + docker.ttio.cloud:5000/library/redis:current +ExecStart=/usr/bin/docker logs -f ttio-redis +ExecStop=/usr/bin/docker stop ttio-redis +ExecStopPost=/usr/bin/docker rm -f ttio-redis diff --git a/packages/ttio-web/units/ttio-survey.service b/packages/ttio-web/units/ttio-survey.service new file mode 100644 index 0000000..cbf8c62 --- /dev/null +++ b/packages/ttio-web/units/ttio-survey.service @@ -0,0 +1,18 @@ +[Unit] +Description=timetab.io Survey +After=ttio-redis.service ttio-postgres.service ttio-api.service docker.service +Requires=ttio-redis.service ttio-postgres.service ttio-api.service docker.service +BindsTo=ttio-web.target + +[Service] +Restart=always +ExecStartPre=/usr/bin/touch /data/nsalog/survey.txt +ExecStartPre=-/usr/bin/docker rm -f ttio-survey +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-survey \ + --net ttio-net \ + -v /data/nsalog/survey.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/survey:current +ExecStart=/usr/bin/docker logs -f ttio-survey +ExecStop=/usr/bin/docker stop ttio-survey +ExecStopPost=/usr/bin/docker rm -f ttio-survey diff --git a/packages/ttio-web/units/ttio-web.target b/packages/ttio-web/units/ttio-web.target new file mode 100644 index 0000000..923331d --- /dev/null +++ b/packages/ttio-web/units/ttio-web.target @@ -0,0 +1,12 @@ +[Unit] +Description=timetab.io Web +After=docker.service +Requires=docker.service +Wants=ttio-api.service \ + ttio-frontend.service \ + ttio-proxy.service \ + ttio-survey.service \ + ttio-workers.target + +[Install] +WantedBy=default.target diff --git a/packages/ttio-web/units/ttio-worker@.service b/packages/ttio-web/units/ttio-worker@.service new file mode 100644 index 0000000..533fb1e --- /dev/null +++ b/packages/ttio-web/units/ttio-worker@.service @@ -0,0 +1,18 @@ +[Unit] +Description=timetab.io Worker +After=ttio-redis.service docker.service +Requires=ttio-redis.service ttio-postgres.service ttio-elastic.service docker.service +BindsTo=ttio-workers.target + +[Service] +Restart=always +ExecStartPre=/usr/bin/touch /data/nsalog/worker-%I.txt +ExecStartPre=-/usr/bin/docker rm -f ttio-worker-%I +ExecStartPre=/usr/bin/docker run -d \ + --name ttio-worker-%I \ + --net ttio-net \ + -v /data/nsalog/worker-%I.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/worker:current +ExecStart=/usr/bin/docker logs -f ttio-worker-%I +ExecStop=/usr/bin/docker stop ttio-worker-%I +ExecStopPost=/usr/bin/docker rm -f ttio-worker-%I diff --git a/packages/ttio-web/units/ttio-workers.target b/packages/ttio-web/units/ttio-workers.target new file mode 100644 index 0000000..39d97da --- /dev/null +++ b/packages/ttio-web/units/ttio-workers.target @@ -0,0 +1,4 @@ +[Unit] +Description=timetab.io Workers +Requires=ttio-worker@0.service ttio-worker@1.service ttio-worker@2.service ttio-worker@3.service ttio-worker@4.service +BindsTo=ttio-web.target diff --git a/rake/gen_autoload.rb b/rake/gen_autoload.rb new file mode 100644 index 0000000..76bb9e7 --- /dev/null +++ b/rake/gen_autoload.rb @@ -0,0 +1,12 @@ +def gen_autoload(directory) + file_name = "#{directory}/autoload.php" + + desc "Generate autoload.php for #{directory}/" + file file_name => FileList["#{directory}/**/*.php"].exclude(file_name) do + sh 'phpab', '-o', file_name, directory + end + + CLEAN.include(file_name) + + file_name +end diff --git a/scripts/attach-workers.sh b/scripts/attach-workers.sh new file mode 100755 index 0000000..9234e94 --- /dev/null +++ b/scripts/attach-workers.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +ROOT=$(cd $(dirname $0)/../ && pwd) + +if [ -z "${1}" ]; then + echo "Usage ${0} COUNT" + exit +fi + +for i in `seq 1 ${1}`; do + docker logs -f ttio-dev-worker-${i} & +done + +wait diff --git a/scripts/build-containers.sh b/scripts/build-containers.sh new file mode 100755 index 0000000..44377b0 --- /dev/null +++ b/scripts/build-containers.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +BASE_DIR=$(cd $(dirname $0)/../ && pwd) + +log () { + tput setaf 2 + echo "--> ${1}..." + tput sgr0 +} + +if [ -z ${VERSION} ]; then + echo "Refusing to build without VERSION. Stop" + exit +fi + +set -e + +log "Building worker" +docker build -t docker.ttio.cloud:5000/web/worker:latest -f "${BASE_DIR}/containers/ttio-worker/Dockerfile" ${BASE_DIR} +docker tag docker.ttio.cloud:5000/web/worker:latest docker.ttio.cloud:5000/web/worker:${VERSION} + +log "Building api" +docker build -t docker.ttio.cloud:5000/web/api:latest -f "${BASE_DIR}/containers/ttio-api/Dockerfile" ${BASE_DIR} +docker tag docker.ttio.cloud:5000/web/api:latest docker.ttio.cloud:5000/web/api:${VERSION} + +log "Building frontend" +docker build --build-arg VERSION=${VERSION} -t docker.ttio.cloud:5000/web/frontend:latest -f "${BASE_DIR}/containers/ttio-frontend/Dockerfile" ${BASE_DIR} +docker tag docker.ttio.cloud:5000/web/frontend:latest docker.ttio.cloud:5000/web/frontend:${VERSION} + +log "Building survey" +docker build --build-arg VERSION=${VERSION} -t docker.ttio.cloud:5000/web/survey:latest -f "${BASE_DIR}/containers/ttio-survey/Dockerfile" ${BASE_DIR} +docker tag docker.ttio.cloud:5000/web/survey:latest docker.ttio.cloud:5000/web/survey:${VERSION} + +log "Building proxy" +docker build --build-arg VERSION=${VERSION} -t docker.ttio.cloud:5000/web/proxy:latest -f "${BASE_DIR}/containers/ttio-proxy/Dockerfile" ${BASE_DIR} +docker tag docker.ttio.cloud:5000/web/proxy:latest docker.ttio.cloud:5000/web/proxy:${VERSION} diff --git a/scripts/build-dev-containers.sh b/scripts/build-dev-containers.sh new file mode 100755 index 0000000..7e24205 --- /dev/null +++ b/scripts/build-dev-containers.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +BASE_DIR=$(cd $(dirname $0)/../ && pwd) + +set -e + +docker build -t ttio-dev-proxy "${BASE_DIR}/containers/ttio-dev-proxy" +docker build -t ttio-dev-frontend "${BASE_DIR}/containers/ttio-dev-frontend" +docker build -t ttio-dev-survey "${BASE_DIR}/containers/ttio-dev-survey" diff --git a/scripts/build-packages.sh b/scripts/build-packages.sh new file mode 100755 index 0000000..a6319ec --- /dev/null +++ b/scripts/build-packages.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +BUILD_DIR="/data/code" +PACKAGE_DIR="${BUILD_DIR}/packages" + +for PACKAGE in ${PACKAGE_DIR}/*; do + RPM_DIR="${PACKAGE}/rpm" + SPEC_FILE="${PACKAGE}/package.spec" + + rm -rf ${RPM_DIR} + mkdir -p ${RPM_DIR} + + if [ "${TTIO_BUILD_ENV}" = 'production' ]; then + tput setaf 3 + echo "Fixing owner root:root on '${SPEC_FILE}'" + tput sgr0 + chown root:root ${SPEC_FILE} + fi + + rpmbuild -ba ${SPEC_FILE} \ + --define "_sourcedir ${BUILD_DIR}" \ + --define "_rpmdir ${RPM_DIR}" \ + --define "_packagedir ${PACKAGE}" + + mv ${RPM_DIR}/noarch/*.rpm ${RPM_DIR}/ + rm -rf ${RPM_DIR}/noarch +done + +tput setaf 2 +echo "Build complete." +tput sgr0 diff --git a/scripts/create-icon.php b/scripts/create-icon.php new file mode 100755 index 0000000..ed31782 --- /dev/null +++ b/scripts/create-icon.php @@ -0,0 +1,42 @@ +#!/usr/bin/env php +loadXML(file_get_contents('php://stdin')); + + $target = new Dom\Document; + $target->formatOutput = true; + $target->preserveWhiteSpace = false; + + foreach ($source->query('//*[@id]') as $node) { + $node->removeAttribute('id'); + } + + $root = $target->appendChild($target->createElement('svg')); + $root->setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + + $symbol = $root->appendChild($target->createElement('symbol')); + $symbol->setAttribute('id', 'icon'); + $symbol->setAttribute('viewBox', '0 0 128 128'); + + $title = $symbol->appendChild($target->createElement('title')); + $title->appendText($argv[1]); + + $source->getXpath()->registerNamespace('svg', 'http://www.w3.org/2000/svg'); + + $topGroup = $source->queryOne('//svg:g'); + + $symbol->appendChild($target->importNode($topGroup, true)); + + $output = $target->saveXML($root); + + echo str_replace('#000000', 'currentColor', $output); +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..1f1371c --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +log () { + tput setaf 2 + echo "--> ${1}..." + tput sgr0 +} + +log "Building containers" +VERSION=${TRAVIS_TAG} ./scripts/build-containers.sh + +log "Pushing containers" +VERSION=${TRAVIS_TAG} ./scripts/push-containers.sh + +log "Building rpm packages with version ${TRAVIS_TAG}" +./scripts/rake.sh VERSION=${TRAVIS_TAG} TTIO_BUILD_ENV=production rpm + +log "Installing rpm packages" +VERSION=${TRAVIS_TAG} ./scripts/release-packages.sh diff --git a/scripts/pull-containers.sh b/scripts/pull-containers.sh new file mode 100755 index 0000000..d5589c4 --- /dev/null +++ b/scripts/pull-containers.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +CONTAINERS=( + library/elastic + library/fpm + library/php + library/postgres + library/redis +) + +for CONTAINER in ${CONTAINERS[@]}; do + docker pull docker.ttio.cloud:5000/${CONTAINER} +done diff --git a/scripts/push-containers.sh b/scripts/push-containers.sh new file mode 100755 index 0000000..6762460 --- /dev/null +++ b/scripts/push-containers.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +CONTAINERS=( + web/worker + web/api + web/frontend + web/proxy + web/survey +) + +if [ -z ${VERSION} ]; then + echo "Refusing to build without VERSION. Stop" + exit +fi + +for CONTAINER in ${CONTAINERS[@]}; do + docker push docker.ttio.cloud:5000/${CONTAINER}:${VERSION} + docker push docker.ttio.cloud:5000/${CONTAINER}:latest +done diff --git a/scripts/push-task.sh b/scripts/push-task.sh new file mode 100755 index 0000000..877a33c --- /dev/null +++ b/scripts/push-task.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec ttio-dev-worker /data/code/Worker/push.php $1 diff --git a/scripts/rake.sh b/scripts/rake.sh new file mode 100755 index 0000000..785d0ea --- /dev/null +++ b/scripts/rake.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker run -v $(pwd):/data/code --rm -it docker.ttio.cloud:5000/build env TTIO_BUILD_ENV=${TTIO_BUILD_ENV} rake ${@} diff --git a/scripts/release-packages.sh b/scripts/release-packages.sh new file mode 100755 index 0000000..0726fa7 --- /dev/null +++ b/scripts/release-packages.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -e + +HOST='root@timetab.io' + +if [ -z ${TTIO_SSH_KEY} ]; then + TTIO_SSH_KEY="${TRAVIS_BUILD_DIR}/data/timetabio-bot" +fi + +if [ -z ${TTIO_RELEASE_TARGET} ]; then + TTIO_RELEASE_TARGET='ssh' +fi + +log() { + tput setaf 2 + echo "${1}" + tput sgr0 +} + +exec_command () { + if [ ${TTIO_RELEASE_TARGET} = 'docker' ]; then + docker exec -it ttio-staging bash -c "${1}" + else + ssh -i ${TTIO_SSH_KEY} -t ${HOST} "${1}" + fi +} + +upload_file () { + if [ ${TTIO_RELEASE_TARGET} = 'docker' ]; then + docker cp ${1} ttio-staging:${2} + else + scp -i ${TTIO_SSH_KEY} ${1} ${HOST}:${2} + fi +} + +UPLOADDIR="/data/rpms/${VERSION}" + +exec_command "mkdir -p ${UPLOADDIR}" + +log "Uploading packages to '${UPLOADDIR}'..." + +upload_file ./packages/ttio-server/rpm/*.rpm ${UPLOADDIR}/ +upload_file ./packages/ttio-web/rpm/*.rpm ${UPLOADDIR}/ +upload_file ./packages/ttio-docker-registry/rpm/*.rpm ${UPLOADDIR}/ + +log "Installing packages..." + +exec_command " + PACKAGES=( + ttio-web + ttio-server + ttio-docker-registry + ) + + for PACKAGE in \${PACKAGES[@]}; do + OLD_VERSION=\$(rpm -qa --queryformat '%{version}' \${PACKAGE}) + + rpm -Uvh ${UPLOADDIR}/\${PACKAGE}-*.rpm + + if [ $? -ne 0 ]; then + rpm -Uvh --oldpackage /data/rpms/\${OLD_VERSION}/\${PACKAGE}-*.rpm + fi + done +" diff --git a/scripts/resty-auth.sh b/scripts/resty-auth.sh new file mode 100755 index 0000000..8ae3d2b --- /dev/null +++ b/scripts/resty-auth.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +function resty_auth() { + printf "Username > " + read USERNAME + + printf "Passsword > " + read -s PASSWORD + + TOKEN_JSON=$(curl --silent -X POST https://devapi.timetab.io/v1/auth -d user=${USERNAME} -d password="${PASSWORD}") + + printf "\n" + echo ${TOKEN_JSON} + + TOKEN=$(echo ${TOKEN_JSON} | python -c "import json, sys; obj = json.load(sys.stdin); print obj['access_token'];") + + resty https://devapi.timetab.io/v1 -H "Authorization: Bearer ${TOKEN}" +} diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..aea6765 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +git clone git@github.com:timetabio/config diff --git a/scripts/spawn-workers.sh b/scripts/spawn-workers.sh new file mode 100755 index 0000000..09e3ae1 --- /dev/null +++ b/scripts/spawn-workers.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +ROOT=$(cd $(dirname $0)/../ && pwd) + +if [ -z "${1}" ]; then + echo "Usage ${0} COUNT" + exit +fi + +docker rm -f $(docker ps -aq --filter="name=ttio-dev-worker-") + +for i in `seq 0 ${1}`; do + docker run -d \ + --name ttio-dev-worker-${i} \ + --net ttio-dev-net \ + -v ${ROOT}:/data/code \ + -v ${ROOT}/persistent/nsalog/worker.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/worker & +done + +wait diff --git a/scripts/staging-release.sh b/scripts/staging-release.sh new file mode 100755 index 0000000..45ed7be --- /dev/null +++ b/scripts/staging-release.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +if [ -z ${VERSION} ]; then + echo "Refusing to run without VERSION. Stop" + exit +fi + +log () { + tput setaf 2 + echo "--> ${1}..." + tput sgr0 +} + +KEEP_STAGING=0 +FETCH_CERTS=1 +SKIP_CONTAINERS=0 + +for ARG in $@; do + case ${ARG} in + --keep) + KEEP_STAGING=1 + ;; + --no-fetch) + FETCH_CERTS=0 + ;; + --skip-containers) + SKIP_CONTAINERS=1 + esac +done + +./scripts/start-the-magic.sh --stop + +log "Fetching letsencrypt data" +if [ ${FETCH_CERTS} -eq 1 ]; then + rm -rf ./persistent/letsencrypt + mkdir -p ./persistent/letsencrypt/live + scp -r root@timetab.io:/etc/letsencrypt/live ./persistent/letsencrypt/ +fi + +log "Preparing staging container" +if [ ${KEEP_STAGING} -eq 0 ]; then + docker build -t ttio-staging ./containers/ttio-staging + docker rm -f ttio-staging +fi + +docker run -d \ + -p 80:80 \ + -p 443:443 \ + -v $(pwd)/persistent/letsencrypt/live:/etc/letsencrypt/live \ + --name ttio-staging \ + --privileged \ + ttio-staging + +if [ ${KEEP_STAGING} -eq 0 ]; then + log "Authenticating with docker registry" + docker exec -it ttio-staging sh -c 'docker login docker.ttio.cloud:5000' +fi + +set -e + +if [ ${SKIP_CONTAINERS} -eq 0 ]; then + log "Building containers" + ./scripts/build-containers.sh + + log "Pushing containers" + ./scripts/push-containers.sh +fi + +log "Building rpms" +./scripts/rake.sh VERSION=${VERSION} rpm + +log "Releasing rpms" + +docker exec -it ttio-staging rpm -e ttio-web || true +docker exec -it ttio-staging rpm -e ttio-server || true + +TTIO_RELEASE_TARGET='docker' ./scripts/release-packages.sh diff --git a/scripts/start-the-magic.sh b/scripts/start-the-magic.sh new file mode 100755 index 0000000..4bb5fb4 --- /dev/null +++ b/scripts/start-the-magic.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +ROOT=$(cd $(dirname $0)/../ && pwd) + +STOP=0 +BUILD=0 +WORKER=0 +INITIAL=0 + +CONTAINERS=( + ttio-dev-proxy + ttio-dev-api + ttio-dev-frontend + ttio-dev-survey + ttio-dev-redis + ttio-dev-postgres + ttio-elastic + ttio-dev-worker +) + +for ARG in $@; do + case ${ARG} in + --stop) + STOP=1 + ;; + --build) + BUILD=1 + ;; + --worker) + WORKER=1 + ;; + --initial) + INITIAL=1 + ;; + *) + echo "Unknown flag ${ARG}" + exit 1 + esac +done + +log () { + tput setaf 2 + echo "--> ${1}..." + tput sgr0 +} + +await_output () { + FILE=$(mktemp) + docker logs -f "${1}" > ${FILE} 2>&1 & + PID=$! + grep -m 1 "${2}" <(tail -f ${FILE}) + kill ${PID} + rm ${FILE} +} + +mkdir -p ${ROOT}/persistent/nsalog + +touch ${ROOT}/persistent/nsalog/api.txt +touch ${ROOT}/persistent/nsalog/frontend.txt +touch ${ROOT}/persistent/nsalog/worker.txt +touch ${ROOT}/persistent/nsalog/survey.txt + +if [ ${BUILD} -eq 1 ]; then + # VERSION=$(git describe --abbrev=0 --tags) ${ROOT}/scripts/build-containers.sh + VERSION='0.0.1' ${ROOT}/scripts/build-containers.sh + ${ROOT}/scripts/build-dev-containers.sh +fi + +if [ -z $(docker network inspect --format '{{.Name}}' ttio-dev-net 2> /dev/null) ]; then + log "Creating network" + docker network create ttio-dev-net +fi + +log "Stopping running containers" +if [ ${WORKER} -eq 1 ]; then + docker rm -f ttio-dev-worker & +else + docker rm -f $(docker ps -q --filter="name=ttio-dev-worker-") + + for CONTAINER in ${CONTAINERS[@]}; do + docker rm -f ${CONTAINER} & + done +fi + +wait + +if [ ${STOP} -eq 1 ]; then + exit +fi + +log "Starting redis" +docker run -d \ + --name ttio-dev-redis \ + --net ttio-dev-net \ + -v ${ROOT}/persistent/redis:/data \ + docker.ttio.cloud:5000/library/redis & + +log "Starting postgres" +docker run -d \ + --name ttio-dev-postgres \ + --net ttio-dev-net \ + -p 127.0.0.1:5432:5432 \ + -v ${ROOT}/persistent/postgres:/var/lib/postgresql/data \ + docker.ttio.cloud:5000/library/postgres & + +log "Starting elastic" +docker run -d \ + --name ttio-elastic \ + --net ttio-dev-net \ + -v ${ROOT}/persistent/elastic:/usr/share/elasticsearch/data \ + -v ${ROOT}/containers/ttio-elastic/mappings.json:/data/mappings.json \ + -p 9200:9200 \ + docker.ttio.cloud:5000/library/elastic & + +wait + +log "Starting api" +docker run -d \ + --name ttio-dev-api \ + --net ttio-dev-net \ + -v ${ROOT}:/data/code \ + -v ${ROOT}/persistent/nsalog/api.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/api & + +log "Starting frontend" +docker run -d \ + --name ttio-dev-frontend \ + --net ttio-dev-net \ + -v ${ROOT}:/data/code \ + -v ${ROOT}/persistent/nsalog/frontend.txt:/data/nsalog.txt \ + ttio-dev-frontend & + +log "Starting survey" +docker run -d \ + --name ttio-dev-survey \ + --net ttio-dev-net \ + -v ${ROOT}:/data/code \ + -v ${ROOT}/persistent/nsalog/survey.txt:/data/nsalog.txt \ + ttio-dev-survey & + +wait + +log "Starting nginx" +docker run -d \ + --name ttio-dev-proxy \ + --net ttio-dev-net \ + -p 80:80/tcp \ + -p 443:443/tcp \ + -v ${ROOT}:/var/www \ + ttio-dev-proxy + + +PROXY_IP=$(docker inspect --format '{{with index .NetworkSettings.Networks "ttio-dev-net"}}{{.IPAddress}}{{end}}' ttio-dev-proxy) + +docker exec -it ttio-dev-frontend bash -c "echo '${PROXY_IP} devapi.timetab.io' >> /etc/hosts" +docker exec -it ttio-dev-survey bash -c "echo '${PROXY_IP} devapi.timetab.io' >> /etc/hosts" + +log "Generating system token" +docker exec -it ttio-dev-api /data/code/API/scripts/create-system-token.php + +log "Waiting for postgres to start" +await_output ttio-dev-postgres "database system is ready to accept connections" + +if [ ${INITIAL} -eq 1 ]; then + log "Bootstrapping postgres" + docker run --rm \ + --net ttio-dev-net \ + -v ${ROOT}/data/schema.sql:/schema.sql \ + docker.ttio.cloud:5000/library/postgres \ + env psql -U postgres -h ttio-dev-postgres -a -f /schema.sql +fi + +docker run --rm \ + --net ttio-dev-net \ + -v ${ROOT}/data/patches:/data/patches \ + -v ${ROOT}/persistent/applied-patches:/data/applied \ + docker.ttio.cloud:5000/library/postgres \ + env TERM=xterm POSTGRES_HOST=ttio-dev-postgres ttio-patch + +log "Waiting for elastic to start" +await_output ttio-elastic ":9200" + +log "Starting worker" +docker run -d \ + --name ttio-dev-worker \ + --net ttio-dev-net \ + -v ${ROOT}:/data/code \ + -v ${ROOT}/persistent/nsalog/worker.txt:/data/nsalog.txt \ + docker.ttio.cloud:5000/web/worker + +log "Bootstrapping elastic" +docker run --rm \ + --net ttio-dev-net \ + -v ${ROOT}/data/elastic-mappings.json:/mappings.json \ + docker.ttio.cloud:5000/library/elastic \ + sh -c 'curl -X PUT http://ttio-elastic:9200/ttio -d @/mappings.json' +echo "" + +if [ ${WORKER} -eq 0 ]; then + log "Running initial task" + docker exec ttio-dev-worker /data/code/Worker/push.php Initial +fi