From 3d89b294c149b1b616775257704867df12a9af5e Mon Sep 17 00:00:00 2001 From: Emma Date: Sun, 25 Mar 2018 03:14:46 +0200 Subject: [PATCH 01/24] update dependencies (fixes broken comments page) --- composer.lock | 239 ++++++++++++++++++++++++++------------------------ 1 file changed, 123 insertions(+), 116 deletions(-) diff --git a/composer.lock b/composer.lock index 8e5e0b1..570f35f 100644 --- a/composer.lock +++ b/composer.lock @@ -1467,16 +1467,16 @@ }, { "name": "league/commonmark", - "version": "0.17.0", + "version": "0.17.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "3b4c2224524776a584de663c7a04bc8eb2e1544d" + "reference": "38b2fca6b8493abd9314fcf6b5b042cfba6a0f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3b4c2224524776a584de663c7a04bc8eb2e1544d", - "reference": "3b4c2224524776a584de663c7a04bc8eb2e1544d", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/38b2fca6b8493abd9314fcf6b5b042cfba6a0f11", + "reference": "38b2fca6b8493abd9314fcf6b5b042cfba6a0f11", "shasum": "" }, "require": { @@ -1491,10 +1491,10 @@ "commonmark/commonmark.js": "0.28", "erusev/parsedown": "~1.0", "michelf/php-markdown": "~1.4", - "mikehaertl/php-shellcommand": "~1.2.0", - "phpunit/phpunit": "~5.7|~6.5", - "scrutinizer/ocular": "~1.1", - "symfony/finder": "~3.0|~4.0" + "mikehaertl/php-shellcommand": "^1.2", + "phpunit/phpunit": "^5.7|^6.5", + "scrutinizer/ocular": "^1.1", + "symfony/finder": "^3.0|^4.0" }, "suggest": { "league/commonmark-extras": "Library of useful extensions including smart punctuation" @@ -1532,7 +1532,7 @@ "markdown", "parser" ], - "time": "2017-12-30T22:08:48+00:00" + "time": "2018-03-18T13:01:30+00:00" }, { "name": "league/flysystem", @@ -1624,12 +1624,12 @@ "source": { "type": "git", "url": "https://github.com/liip/LiipImagineBundle.git", - "reference": "13cdab6e01daeb945e690e2be8d3a627489b02a2" + "reference": "7700c5a82ff900189d00bedbc3d5bde9efbb4990" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/13cdab6e01daeb945e690e2be8d3a627489b02a2", - "reference": "13cdab6e01daeb945e690e2be8d3a627489b02a2", + "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/7700c5a82ff900189d00bedbc3d5bde9efbb4990", + "reference": "7700c5a82ff900189d00bedbc3d5bde9efbb4990", "shasum": "" }, "require": { @@ -1716,7 +1716,7 @@ "symfony", "transformation" ], - "time": "2018-03-02T20:07:20+00:00" + "time": "2018-03-21T08:53:31+00:00" }, { "name": "monolog/monolog", @@ -2469,16 +2469,16 @@ }, { "name": "psr/simple-cache", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", - "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", "shasum": "" }, "require": { @@ -2513,7 +2513,7 @@ "psr-16", "simple-cache" ], - "time": "2017-01-02T13:31:39+00:00" + "time": "2017-10-23T01:57:42+00:00" }, { "name": "ramsey/uuid", @@ -2877,7 +2877,7 @@ }, { "name": "symfony/asset", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", @@ -2933,7 +2933,7 @@ }, { "name": "symfony/cache", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", @@ -3002,7 +3002,7 @@ }, { "name": "symfony/config", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -3064,7 +3064,7 @@ }, { "name": "symfony/console", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -3132,7 +3132,7 @@ }, { "name": "symfony/debug", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -3188,16 +3188,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "2fca7961d10cf33fa528ce633e992942ef1bccd8" + "reference": "93ad14f124beacf16894b64bb5b3cdd5b4367e38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2fca7961d10cf33fa528ce633e992942ef1bccd8", - "reference": "2fca7961d10cf33fa528ce633e992942ef1bccd8", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/93ad14f124beacf16894b64bb5b3cdd5b4367e38", + "reference": "93ad14f124beacf16894b64bb5b3cdd5b4367e38", "shasum": "" }, "require": { @@ -3255,11 +3255,11 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-02-26T15:55:47+00:00" + "time": "2018-03-05T18:28:26+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", @@ -3338,7 +3338,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3401,7 +3401,7 @@ }, { "name": "symfony/expression-language", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", @@ -3451,7 +3451,7 @@ }, { "name": "symfony/filesystem", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3500,16 +3500,16 @@ }, { "name": "symfony/finder", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "552e244df10237f845a94fd64b194f848805e34b" + "reference": "44a796d2ecc2a16a5fc8f2956a34ee617934d55f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/552e244df10237f845a94fd64b194f848805e34b", - "reference": "552e244df10237f845a94fd64b194f848805e34b", + "url": "https://api.github.com/repos/symfony/finder/zipball/44a796d2ecc2a16a5fc8f2956a34ee617934d55f", + "reference": "44a796d2ecc2a16a5fc8f2956a34ee617934d55f", "shasum": "" }, "require": { @@ -3545,20 +3545,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-02-11T17:17:44+00:00" + "time": "2018-03-05T18:28:26+00:00" }, { "name": "symfony/flex", - "version": "v1.0.70", + "version": "v1.0.76", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "1f00c05d35523dc0ac52e4a457989a069be5a7a4" + "reference": "202c981673b255a41bac4076cb82629a0b8481cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/1f00c05d35523dc0ac52e4a457989a069be5a7a4", - "reference": "1f00c05d35523dc0ac52e4a457989a069be5a7a4", + "url": "https://api.github.com/repos/symfony/flex/zipball/202c981673b255a41bac4076cb82629a0b8481cb", + "reference": "202c981673b255a41bac4076cb82629a0b8481cb", "shasum": "" }, "require": { @@ -3591,11 +3591,11 @@ "email": "fabien.potencier@gmail.com" } ], - "time": "2018-02-22T07:00:47+00:00" + "time": "2018-03-23T16:30:50+00:00" }, { "name": "symfony/form", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/form.git", @@ -3675,16 +3675,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "1221c54863061e71573b07882b4c53795fbfc229" + "reference": "d47d6da8c852648e26f12e55e4c895b81c4e99bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/1221c54863061e71573b07882b4c53795fbfc229", - "reference": "1221c54863061e71573b07882b4c53795fbfc229", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d47d6da8c852648e26f12e55e4c895b81c4e99bf", + "reference": "d47d6da8c852648e26f12e55e4c895b81c4e99bf", "shasum": "" }, "require": { @@ -3785,20 +3785,20 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2018-02-28T21:50:02+00:00" + "time": "2018-03-02T08:28:17+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "94139989e51193e62a46bc87741ae05c8e8390f5" + "reference": "6c181e81a3a9a7996c62ebd7803592536e729c5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/94139989e51193e62a46bc87741ae05c8e8390f5", - "reference": "94139989e51193e62a46bc87741ae05c8e8390f5", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6c181e81a3a9a7996c62ebd7803592536e729c5a", + "reference": "6c181e81a3a9a7996c62ebd7803592536e729c5a", "shasum": "" }, "require": { @@ -3838,20 +3838,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-02-22T10:50:29+00:00" + "time": "2018-03-05T16:01:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "77cf672c855f038b1b916201d00b1d55899ee8e7" + "reference": "2a1ebfe8c37240500befcb17bceb3893adacffa3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/77cf672c855f038b1b916201d00b1d55899ee8e7", - "reference": "77cf672c855f038b1b916201d00b1d55899ee8e7", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2a1ebfe8c37240500befcb17bceb3893adacffa3", + "reference": "2a1ebfe8c37240500befcb17bceb3893adacffa3", "shasum": "" }, "require": { @@ -3924,11 +3924,11 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-03-01T19:48:35+00:00" + "time": "2018-03-05T22:27:01+00:00" }, { "name": "symfony/inflector", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/inflector.git", @@ -3985,7 +3985,7 @@ }, { "name": "symfony/intl", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", @@ -4060,7 +4060,7 @@ }, { "name": "symfony/monolog-bridge", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", @@ -4126,16 +4126,16 @@ }, { "name": "symfony/monolog-bundle", - "version": "v3.1.2", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bundle.git", - "reference": "2b41b8b6d2c6edb1a5494f02f8e4129be2a44784" + "reference": "8781649349fe418d51d194f8c9d212c0b97c40dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/2b41b8b6d2c6edb1a5494f02f8e4129be2a44784", - "reference": "2b41b8b6d2c6edb1a5494f02f8e4129be2a44784", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/8781649349fe418d51d194f8c9d212c0b97c40dd", + "reference": "8781649349fe418d51d194f8c9d212c0b97c40dd", "shasum": "" }, "require": { @@ -4185,11 +4185,11 @@ "log", "logging" ], - "time": "2017-11-06T16:02:17+00:00" + "time": "2018-03-05T14:51:36+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -4448,7 +4448,7 @@ }, { "name": "symfony/process", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -4497,7 +4497,7 @@ }, { "name": "symfony/property-access", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", @@ -4564,7 +4564,7 @@ }, { "name": "symfony/property-info", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", @@ -4688,7 +4688,7 @@ }, { "name": "symfony/routing", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -4766,7 +4766,7 @@ }, { "name": "symfony/security", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/security.git", @@ -4843,7 +4843,7 @@ }, { "name": "symfony/security-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", @@ -4923,7 +4923,7 @@ }, { "name": "symfony/serializer", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", @@ -5032,16 +5032,16 @@ }, { "name": "symfony/swiftmailer-bundle", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/swiftmailer-bundle.git", - "reference": "7756867d9b21231ae0ce746a6b861b7e293cc885" + "reference": "20e71c247a5a43ceb655db9712394d08c09b33ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/7756867d9b21231ae0ce746a6b861b7e293cc885", - "reference": "7756867d9b21231ae0ce746a6b861b7e293cc885", + "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/20e71c247a5a43ceb655db9712394d08c09b33ef", + "reference": "20e71c247a5a43ceb655db9712394d08c09b33ef", "shasum": "" }, "require": { @@ -5090,11 +5090,11 @@ ], "description": "Symfony SwiftmailerBundle", "homepage": "http://symfony.com", - "time": "2018-02-14T08:09:24+00:00" + "time": "2018-03-08T16:39:26+00:00" }, { "name": "symfony/templating", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/templating.git", @@ -5149,7 +5149,7 @@ }, { "name": "symfony/translation", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -5217,7 +5217,7 @@ }, { "name": "symfony/twig-bridge", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", @@ -5307,7 +5307,7 @@ }, { "name": "symfony/twig-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", @@ -5380,7 +5380,7 @@ }, { "name": "symfony/validator", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", @@ -5464,7 +5464,7 @@ }, { "name": "symfony/web-link", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", @@ -5535,7 +5535,7 @@ }, { "name": "symfony/yaml", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -5649,16 +5649,16 @@ }, { "name": "twig/twig", - "version": "v2.4.6", + "version": "v2.4.7", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f" + "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/d2117ec118c1ff3d28ccddca8212d82787a4809f", - "reference": "d2117ec118c1ff3d28ccddca8212d82787a4809f", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/69aacd44dbbaa3199d5afb68605c996d577896fc", + "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc", "shasum": "" }, "require": { @@ -5667,8 +5667,8 @@ }, "require-dev": { "psr/container": "^1.0", - "symfony/debug": "~2.7", - "symfony/phpunit-bridge": "~3.3@dev" + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.3" }, "type": "library", "extra": { @@ -5711,7 +5711,7 @@ "keywords": [ "templating" ], - "time": "2018-03-03T16:23:01+00:00" + "time": "2018-03-20T04:31:17+00:00" }, { "name": "webmozart/assert", @@ -6373,16 +6373,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.10.3", + "version": "v2.11.1", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1634a2c250bf4640f1c5c963f63b413c2d966c8a" + "reference": "ad94441c17b8ef096e517acccdbf3238af8a2da8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1634a2c250bf4640f1c5c963f63b413c2d966c8a", - "reference": "1634a2c250bf4640f1c5c963f63b413c2d966c8a", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/ad94441c17b8ef096e517acccdbf3238af8a2da8", + "reference": "ad94441c17b8ef096e517acccdbf3238af8a2da8", "shasum": "" }, "require": { @@ -6391,7 +6391,7 @@ "ext-json": "*", "ext-tokenizer": "*", "php": "^5.6 || >=7.0 <7.3", - "php-cs-fixer/diff": "^1.2", + "php-cs-fixer/diff": "^1.3", "symfony/console": "^3.2 || ^4.0", "symfony/event-dispatcher": "^3.0 || ^4.0", "symfony/filesystem": "^3.0 || ^4.0", @@ -6406,14 +6406,14 @@ "hhvm": "*" }, "require-dev": { - "johnkary/phpunit-speedtrap": "^1.1 || ^2.0@dev", + "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", "keradus/cli-executor": "^1.0", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.0", "php-cs-fixer/accessible-object": "^1.0", - "phpunit/phpunit": "^5.7.23 || ^6.4.3", - "phpunitgoodpractices/traits": "^1.0", + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", + "phpunitgoodpractices/traits": "^1.3.1", "symfony/phpunit-bridge": "^3.2.2 || ^4.0" }, "suggest": { @@ -6424,6 +6424,11 @@ "php-cs-fixer" ], "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.11-dev" + } + }, "autoload": { "psr-4": { "PhpCsFixer\\": "src/" @@ -6434,6 +6439,8 @@ "tests/Test/AbstractIntegrationTestCase.php", "tests/Test/Assert/AssertTokensTrait.php", "tests/Test/Constraint/SameStringsConstraint.php", + "tests/Test/Constraint/SameStringsConstraintForV5.php", + "tests/Test/Constraint/SameStringsConstraintForV7.php", "tests/Test/IntegrationCase.php", "tests/Test/IntegrationCaseFactory.php", "tests/Test/IntegrationCaseFactoryInterface.php", @@ -6456,7 +6463,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2018-02-22T16:49:33+00:00" + "time": "2018-03-21T17:41:26+00:00" }, { "name": "php-cs-fixer/diff", @@ -6511,7 +6518,7 @@ }, { "name": "symfony/browser-kit", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -6568,7 +6575,7 @@ }, { "name": "symfony/css-selector", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -6621,7 +6628,7 @@ }, { "name": "symfony/debug-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", @@ -6717,7 +6724,7 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -6773,7 +6780,7 @@ }, { "name": "symfony/dotenv", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", @@ -6830,7 +6837,7 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", @@ -6979,7 +6986,7 @@ }, { "name": "symfony/stopwatch", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -7028,7 +7035,7 @@ }, { "name": "symfony/var-dumper", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", @@ -7097,16 +7104,16 @@ }, { "name": "symfony/web-profiler-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "cd27b1813b0ce6b042b411cbb4e9d2aad6f07027" + "reference": "eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/cd27b1813b0ce6b042b411cbb4e9d2aad6f07027", - "reference": "cd27b1813b0ce6b042b411cbb4e9d2aad6f07027", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de", + "reference": "eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de", "shasum": "" }, "require": { @@ -7159,11 +7166,11 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2018-02-28T21:50:02+00:00" + "time": "2018-03-02T08:28:17+00:00" }, { "name": "symfony/web-server-bundle", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/symfony/web-server-bundle.git", From 22575f53fd8e79b35dd6d84fc373332d8bbf69f1 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 30 Mar 2018 06:31:51 +0200 Subject: [PATCH 02/24] prevent long site names from wrapping --- assets/less/components/site-nav.less | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/less/components/site-nav.less b/assets/less/components/site-nav.less index e228042..98c6374 100644 --- a/assets/less/components/site-nav.less +++ b/assets/less/components/site-nav.less @@ -17,6 +17,7 @@ display: block; font-size: @nav-font-size; padding: 1rem; + white-space: nowrap; } &__logo { From 5b928aee944dfe4d6c3b3ace5b91b8daed91eb2c Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 30 Mar 2018 06:51:28 +0200 Subject: [PATCH 03/24] update notice/new project URL --- README.md | 29 ++++++++--------------------- assets/less/main.less | 2 +- templates/base.html.twig | 2 +- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a15a481..87f49e5 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,7 @@ -**Notice**: Now that the community Postmill was created for has become -unpalatable for me, the future of this project is uncertain. Previously, I -could spend time implementing a feature or improving something, and see the -community embrace (or, on occasion, deride) my hard work. Now that I have no -community, this is no longer the case, and I cannot imagine what motivation I -could have to keep working on this project. - -For now, if you want to report issues or submit pull requests, I'll take a look -at them. Any security holes I become aware of (either in Postmill itself or in -its dependencies) will be fixed. But active development probably won't happen -unless someone steps up. Consider Postmill to be on life support. - -I'm passionate about building communities, and I will probably make another -effort to build something cool in the future, using the experience I made from -Postmill. Until then, I'm taking a break. - -Thanks to everyone who contributed to Postmill, or merely sent kind words my -way. You made it all worth it. - --- Emma +Thanks to continued interest in Postmill, as well as some community efforts to +improve it, I feel a bit more motivated to work on this project again. A +[website][website] and new [support forums][support] have been set up. We'll see +where things go from here. *** @@ -75,7 +59,8 @@ Clone the repository somewhere and navigate there with the command line. ## Reporting issues * Bugs and feature requests should be reported on the [issue tracker][issues]. -* Get support on Postmill's Matrix channel: [#postmill:matrix.org][matrix]. +* Get support on Postmill's [support board][support] or + [Matrix channel][matrix]. You can email emma1312@protonmail.ch to disclose or discuss something in private with the creator of the software. @@ -98,3 +83,5 @@ details. [issues]: https://gitlab.com/edgyemma/Postmill/issues [matrix]: https://matrix.to/#/#postmill:matrix.org +[support]: https://community.postmill.xyz/f/Support +[website]: https://postmill.xyz/ diff --git a/assets/less/main.less b/assets/less/main.less index 1a541bb..2f7dee6 100644 --- a/assets/less/main.less +++ b/assets/less/main.less @@ -1,6 +1,6 @@ @import (less) '~normalize.css'; -/*! https://gitlab.com/edgyemma/Postmill */ +/*! https://postmill.xyz/ */ // global element styles (must be first) @import 'global'; diff --git a/templates/base.html.twig b/templates/base.html.twig index 2914210..6c8218b 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -49,7 +49,7 @@ {{ 'site_footer.version'|trans({ '%branch%': app_branch(), '%version%': app_version(), - '%app%': 'Postmill', + '%app%': 'Postmill', })|raw }}

From c436a963720c83e8c179549cc78bfcfee0bedb89 Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 31 Mar 2018 06:14:19 +0200 Subject: [PATCH 04/24] added webhooks --- .env.dist | 1 + composer.json | 1 + composer.lock | 294 +++++++++++++++++++++- config/app_routes/forum.yaml | 20 ++ config/bundles.php | 1 + config/packages/eight_points_guzzle.yaml | 16 ++ config/services.yaml | 11 + docs/webhooks.md | 178 +++++++++++++ src/Controller/CommentController.php | 36 ++- src/Controller/ForumController.php | 143 +++++++++++ src/Controller/SubmissionController.php | 41 ++- src/Entity/Forum.php | 27 ++ src/Entity/ForumWebhook.php | 105 ++++++++ src/Entity/UserFlags.php | 23 +- src/Event/EntityModifiedEvent.php | 27 ++ src/EventListener/WebhookListener.php | 184 ++++++++++++++ src/Events.php | 12 + src/Form/ForumWebhookType.php | 46 ++++ src/Form/Model/ForumWebhookData.php | 65 +++++ src/Migrations/Version20180325060258.php | 23 ++ src/Repository/ForumWebhookRepository.php | 14 ++ src/Serializer/CommentNormalizer.php | 85 +++++++ src/Serializer/SubmissionNormalizer.php | 107 ++++++++ src/Twig/AppExtension.php | 11 +- symfony.lock | 21 ++ templates/forum/_webhook_form.html.twig | 9 + templates/forum/add_webhook.html.twig | 15 ++ templates/forum/base.html.twig | 7 + templates/forum/edit_webhook.html.twig | 17 ++ templates/forum/webhooks.html.twig | 57 +++++ tests/Event/EntityModifiedEventTest.php | 22 ++ translations/messages.en.yml | 16 ++ 32 files changed, 1609 insertions(+), 26 deletions(-) create mode 100644 config/packages/eight_points_guzzle.yaml create mode 100644 docs/webhooks.md create mode 100644 src/Entity/ForumWebhook.php create mode 100644 src/Event/EntityModifiedEvent.php create mode 100644 src/EventListener/WebhookListener.php create mode 100644 src/Events.php create mode 100644 src/Form/ForumWebhookType.php create mode 100644 src/Form/Model/ForumWebhookData.php create mode 100644 src/Migrations/Version20180325060258.php create mode 100644 src/Repository/ForumWebhookRepository.php create mode 100644 src/Serializer/CommentNormalizer.php create mode 100644 src/Serializer/SubmissionNormalizer.php create mode 100644 templates/forum/_webhook_form.html.twig create mode 100644 templates/forum/add_webhook.html.twig create mode 100644 templates/forum/edit_webhook.html.twig create mode 100644 templates/forum/webhooks.html.twig create mode 100644 tests/Event/EntityModifiedEventTest.php diff --git a/.env.dist b/.env.dist index a0191ee..217bf0e 100644 --- a/.env.dist +++ b/.env.dist @@ -5,6 +5,7 @@ SITE_NAME=Postmill NO_REPLY_ADDRESS="no-reply@example.com" APP_LOCALE=en +APP_ENABLE_WEBHOOKS=0 ###> symfony/framework-bundle ### APP_ENV=dev diff --git a/composer.json b/composer.json index 237d465..d882b8c 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "ext-iconv": "*", "ext-pdo_pgsql": "*", "doctrine/doctrine-migrations-bundle": "^1.2", + "eightpoints/guzzle-bundle": "^7.3", "embed/embed": "^3.0", "ezyang/htmlpurifier": "^4.8", "friendsofsymfony/jsrouting-bundle": "^2.1", diff --git a/composer.lock b/composer.lock index 570f35f..ad67ac0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "357889276824e57ec51081f41102f414", + "content-hash": "05cf9ecf6e555b98c3adb6f3f8d9ced3", "packages": [ { "name": "composer/ca-bundle", @@ -1037,6 +1037,67 @@ ], "time": "2017-11-15T23:40:40+00:00" }, + { + "name": "eightpoints/guzzle-bundle", + "version": "7.3.0", + "source": { + "type": "git", + "url": "https://github.com/8p/EightPointsGuzzleBundle.git", + "reference": "1b663153f860214f18a18d8df6bf5f0592689550" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/8p/EightPointsGuzzleBundle/zipball/1b663153f860214f18a18d8df6bf5f0592689550", + "reference": "1b663153f860214f18a18d8df6bf5f0592689550", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.0", + "php": ">=7.0", + "psr/log": "~1.0", + "symfony/expression-language": "~2.7|~3.0|~4.0", + "symfony/framework-bundle": "~2.7|~3.0|~4.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.1", + "symfony/var-dumper": "~2.7|~3.0|~4.0", + "twig/twig": "~1.5|~2.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "EightPoints\\Bundle\\GuzzleBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Community", + "homepage": "https://github.com/8p/GuzzleBundle/contributors" + }, + { + "name": "Florian Preusner", + "email": "florian.preusner@8points.de", + "homepage": "https://github.com/florianpreusner" + } + ], + "description": "Integrates Guzzle 6.x, a PHP HTTP Client, into Symfony 2/3/4. Comes with easy and powerful configuration options and optional plugins.", + "homepage": "https://github.com/8p/GuzzleBundle", + "keywords": [ + "Guzzle", + "bundle", + "client", + "curl", + "http client", + "rest", + "symfony", + "web service" + ], + "time": "2018-01-14T20:52:44+00:00" + }, { "name": "embed/embed", "version": "v3.3.1", @@ -1357,6 +1418,187 @@ ], "time": "2017-12-28T11:21:03+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-06-22T18:50:49+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, { "name": "imagine/imagine", "version": "v0.7.1", @@ -2371,6 +2613,56 @@ ], "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, { "name": "psr/link", "version": "1.0.0", diff --git a/config/app_routes/forum.yaml b/config/app_routes/forum.yaml index 3ba4a33..adc578d 100644 --- a/config/app_routes/forum.yaml +++ b/config/app_routes/forum.yaml @@ -119,6 +119,26 @@ forum_list: methods: [GET] requirements: { forums: \d+, sortBy: by_name|by_title|by_subscribers|by_submissions } +forum_webhooks: + controller: App\Controller\ForumController::webhooks + methods: [GET] + path: /f/{forum_name}/webhooks + +forum_add_webhook: + controller: App\Controller\ForumController::addWebhook + methods: [GET, POST] + path: /f/{forum_name}/add_webhook + +forum_edit_webhook: + controller: App\Controller\ForumController::editWebhook + methods: [GET, POST] + path: /f/{forum_name}/edit_webhook/{webhook_id} + +forum_remove_webhook: + controller: App\Controller\ForumController::removeWebhook + methods: [POST] + path: /f/{forum_name}/remove_webhook + forums_by_category: controller: App\Controller\ForumController::listCategories path: /forums/by_category diff --git a/config/bundles.php b/config/bundles.php index fd9362e..a011418 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -21,4 +21,5 @@ Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true], + EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class => ['all' => true], ]; diff --git a/config/packages/eight_points_guzzle.yaml b/config/packages/eight_points_guzzle.yaml new file mode 100644 index 0000000..30f9549 --- /dev/null +++ b/config/packages/eight_points_guzzle.yaml @@ -0,0 +1,16 @@ +eight_points_guzzle: + clients: + webhook_client: + options: + timeout: 5 + + # Configure headers. + # More info: http://docs.guzzlephp.org/en/stable/request-options.html#headers + headers: + User-Agent: "Postmill/0.6 (https://postmill.xyz/)" + Accept: application/json, */* + Content-Type: application/json + + # Find plugins here: + # https://github.com/8p/EightPointsGuzzleBundle#known-and-supported-plugins + plugin: ~ diff --git a/config/services.yaml b/config/services.yaml index cb7b4ba..13af52b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -5,6 +5,7 @@ parameters: user_forum_creation_interval: 1 day uuid_regex: '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' wiki_page_regex: '[A-Za-z][A-Za-z0-9_-]*(/[A-Za-z][A-Za-z0-9_-]*)*' + env(APP_ENABLE_WEBHOOKS): false services: _defaults: @@ -40,6 +41,10 @@ services: arguments: $defaultLocale: "%env(APP_LOCALE)%" + App\Controller\ForumController: + arguments: + $enableWebhooks: "%env(bool:APP_ENABLE_WEBHOOKS)%" + App\Controller\UserController: arguments: $defaultLocale: "%env(APP_LOCALE)%" @@ -63,6 +68,11 @@ services: - { name: doctrine.event_listener, event: postPersist } - { name: kernel.event_listener, event: kernel.terminate } + App\EventListener\WebhookListener: + arguments: + $client: "@eight_points_guzzle.client.webhook_client" + $webhooksEnabled: "%env(bool:APP_ENABLE_WEBHOOKS)%" + App\Form\RequestPasswordResetType: arguments: $bypass: "@=parameter('kernel.environment') === 'test'" @@ -80,6 +90,7 @@ services: App\Twig\AppExtension: arguments: $siteName: "%env(SITE_NAME)%" + $enableWebhooks: "%env(bool:APP_ENABLE_WEBHOOKS)%" App\Utils\CachedMarkdownConverter: arguments: diff --git a/docs/webhooks.md b/docs/webhooks.md new file mode 100644 index 0000000..bf8673a --- /dev/null +++ b/docs/webhooks.md @@ -0,0 +1,178 @@ +# About webhooks + +A [webhook](https://en.wikipedia.org/wiki/Webhook) is a mechanism for notifying +over HTTP a third-party server when an event occurs. For instance, if a webhook +is configured to listen on 'new comment' events with the URL +`http://example.com/`, Postmill will send a POST request to that URL every time +a new comment is posted. + +Currently, webhooks are added on a per-forum basis, and are managed by forum +moderators. The ability to add global webhooks is planned. + +For security reasons, webhooks are disabled by default. If you trust your mods, +or you understand the security implications of letting your server make an +arbitrary number of HTTP requests to arbitrary servers when an event occurs, you +can set the `APP_ENABLE_WEBHOOKS` environment variable to `1` to enable +webhooks. + +Performance +--- + +Webhooks are dispatched when Symfony's `kernel.terminate` event has been +dispatched. In practice, this means that setups which don't use PHP-FPM will +dispatch the webhooks before delivering the response, which can make events +seem slow to users. + +Secret token +--- + +The secret token, if specified, will be included in the outgoing request via the +`X-Postmill-Secret` header. + +Request bodies +--- + +### New submission + +~~~json +{ + "event": "new_submission", + "subject": { + "resource": "https://example.com/f/example/420/smoke-weed", + "id": 420, + "forum": "https://example.com/f/example", + "user": "https://example.com/user/emma", + "title": "Smoke weed!", + "body": "its good for your soul", + "url": "https://foo.example.com/", + "timestamp": "2018-04-20T06:09:00+00:00", + "locked": false, + "sticky": false, + "user_flag": "moderator", + "edited_at": "2018-06-09T04:21:09+00:00", + "moderated": false, + "comment_count": 0, + "upvotes": 0, + "downvotes": 0, + "thumbnail_1x": "https://example.com/very_long_image_url.jpg", + "thumbnail_2x": "https://example.com/very_long_image_url_2x.jpg" + } +} +~~~ + +Note that thumbnails are very unlikely to be available before the webhook has +been dispatched. + +### Edit submission + +~~~json +{ + "event": "edit_submission", + "subject": { + "before": { + "resource": "https://example.com/f/example/420/smoke-weed", + "id": 420, + "forum": "https://example.com/f/example", + "user": "https://example.com/user/emma", + "title": "Smoke weed!", + "body": "its good for your soul", + "url": "https://foo.example.com/", + "timestamp": "2018-04-20T06:09:00+00:00", + "locked": false, + "sticky": false, + "user_flag": "moderator", + "moderated": false, + "comment_count": 69, + "upvotes": 420, + "downvotes": 69, + "thumbnail_1x": "https://example.com/very_long_image_url.jpg", + "thumbnail_2x": "https://example.com/very_long_image_url_2x.jpg" + }, + "after": { + "resource": "https://example.com/f/example/420/smoke-weed", + "id": 420, + "forum": "https://example.com/f/example", + "user": "https://example.com/user/emma", + "title": "Smoke weed!", + "body": "actually don't", + "url": "https://foo.example.com/", + "timestamp": "2018-04-20T06:09:00+00:00", + "locked": false, + "sticky": false, + "user_flag": "moderator", + "edited_at": "2018-06-09T04:21:09+00:00", + "moderated": false, + "comment_count": 69, + "upvotes": 420, + "downvotes": 69, + "thumbnail_1x": "https://example.com/very_long_image_url.jpg", + "thumbnail_2x": "https://example.com/very_long_image_url_2x.jpg" + } + } +} +~~~ + +### New comment + +~~~json +{ + "event": "new_comment", + "subject": { + "resource": "https://example.com/f/example/420/comment/1312", + "id": 1312, + "body": "Raw body of comment", + "timestamp": "2018-04-20T06:09:00+00:00", + "user": "https://example.com/user/emma", + "submission": "https://example.com/f/example/420/smoke-weed", + "parent": "https://example.com/f/example/420/comment/69", + "reply_count": 0, + "upvotes": 0, + "downvotes": 0, + "soft_deleted": false, + "edited_at": "2018-06-09T04:21:09+00:00", + "moderated": false, + "user_flag": "admin" + } +} +~~~ + +### Edit comment + +~~~json +{ + "event": "edit_comment", + "subject": { + "before": { + "resource": "https://example.com/f/example/420/comment/1312", + "id": 1312, + "body": "Smoke a little weed", + "timestamp": "2018-04-20T06:09:00+00:00", + "user": "https://example.com/user/emma", + "submission": "https://example.com/f/example/420/smoke-weed", + "parent": "https://example.com/f/example/420/comment/69", + "reply_count": 42069, + "upvotes": 420, + "downvotes": 69, + "soft_deleted": false, + "moderated": false, + "user_flag": "admin" + }, + "after": { + "resource": "https://example.com/f/example/420/comment/1312", + "id": 1312, + "body": "Smoke lots of weed", + "timestamp": "2018-04-20T06:09:00+00:00", + "user": "https://example.com/user/emma", + "submission": "https://example.com/f/example/420/smoke-weed", + "parent": "https://example.com/f/example/420/comment/69", + "reply_count": 42069, + "upvotes": 420, + "downvotes": 69, + "soft_deleted": false, + "edited_at": "2018-06-09T04:21:09+00:00", + "moderated": false, + "user_flag": "admin" + } + } +} +~~~ diff --git a/src/Controller/CommentController.php b/src/Controller/CommentController.php index b6573b8..b51b54c 100644 --- a/src/Controller/CommentController.php +++ b/src/Controller/CommentController.php @@ -7,6 +7,8 @@ use App\Entity\ForumLogCommentDeletion; use App\Entity\Submission; use App\Entity\User; +use App\Event\EntityModifiedEvent; +use App\Events; use App\Form\CommentType; use App\Form\Model\CommentData; use App\Repository\CommentRepository; @@ -15,6 +17,8 @@ use Doctrine\ORM\EntityManager; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -72,11 +76,12 @@ public function commentForm( * * @IsGranted("ROLE_USER") * - * @param EntityManager $em - * @param Forum $forum - * @param Submission $submission - * @param Request $request - * @param Comment|null $comment + * @param EntityManager $em + * @param Forum $forum + * @param Submission $submission + * @param Request $request + * @param EventDispatcherInterface $dispatcher + * @param Comment|null $comment * * @return Response */ @@ -85,6 +90,7 @@ public function comment( Forum $forum, Submission $submission, Request $request, + EventDispatcherInterface $dispatcher, Comment $comment = null ) { $data = new CommentData(); @@ -100,6 +106,8 @@ public function comment( $em->persist($reply); $em->flush(); + $dispatcher->dispatch(Events::NEW_COMMENT, new GenericEvent($reply)); + return $this->redirectToRoute('comment', [ 'forum_name' => $forum->getName(), 'submission_id' => $submission->getId(), @@ -121,11 +129,12 @@ public function comment( * * @IsGranted("edit", subject="comment") * - * @param EntityManager $em - * @param Forum $forum - * @param Submission $submission - * @param Comment $comment - * @param Request $request + * @param EntityManager $em + * @param Forum $forum + * @param Submission $submission + * @param Comment $comment + * @param Request $request + * @param EventDispatcherInterface $dispatcher * * @return Response */ @@ -134,7 +143,8 @@ public function editComment( Forum $forum, Submission $submission, Comment $comment, - Request $request + Request $request, + EventDispatcherInterface $dispatcher ) { $data = CommentData::createFromComment($comment); @@ -142,10 +152,14 @@ public function editComment( $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $before = clone $comment; $data->updateComment($comment); $em->flush(); + $event = new EntityModifiedEvent($before, $comment); + $dispatcher->dispatch(Events::EDIT_COMMENT, $event); + return $this->redirectToRoute('comment', [ 'forum_name' => $forum->getName(), 'submission_id' => $submission->getId(), diff --git a/src/Controller/ForumController.php b/src/Controller/ForumController.php index b69d1f9..49efcce 100644 --- a/src/Controller/ForumController.php +++ b/src/Controller/ForumController.php @@ -3,13 +3,16 @@ namespace App\Controller; use App\Entity\Forum; +use App\Entity\ForumWebhook; use App\Entity\Moderator; use App\Entity\User; use App\Form\ForumAppearanceType; use App\Form\ForumBanType; use App\Form\ForumType; +use App\Form\ForumWebhookType; use App\Form\Model\ForumBanData; use App\Form\Model\ForumData; +use App\Form\Model\ForumWebhookData; use App\Form\Model\ModeratorData; use App\Form\ModeratorType; use App\Form\PasswordConfirmType; @@ -17,8 +20,10 @@ use App\Repository\ForumCategoryRepository; use App\Repository\ForumLogEntryRepository; use App\Repository\ForumRepository; +use App\Repository\ForumWebhookRepository; use App\Repository\SubmissionRepository; use Doctrine\ORM\EntityManager; +use Ramsey\Uuid\Uuid; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; @@ -29,6 +34,15 @@ * @Entity("user", expr="repository.findOneOrRedirectToCanonical(username, 'username')") */ final class ForumController extends AbstractController { + /** + * @var bool + */ + private $enableWebhooks; + + public function __construct(bool $enableWebhooks) { + $this->enableWebhooks = $enableWebhooks; + } + /** * Show the front page of a given forum. * @@ -387,6 +401,135 @@ public function banHistory(Forum $forum, User $user, int $page = 1) { ]); } + /** + * @IsGranted("moderator", subject="forum") + * + * @param Forum $forum + * + * @return Response + */ + public function webhooks(Forum $forum) { + if (!$this->enableWebhooks) { + throw $this->createNotFoundException('Webhooks are not enabled'); + } + + return $this->render('forum/webhooks.html.twig', [ + 'forum' => $forum, + ]); + } + + /** + * @IsGranted("moderator", subject="forum") + * + * @param Forum $forum + * @param Request $request + * @param EntityManager $em + * + * @return Response + */ + public function addWebhook(Forum $forum, Request $request, EntityManager $em) { + if (!$this->enableWebhooks) { + throw $this->createNotFoundException('Webhooks are not enabled'); + } + + $data = new ForumWebhookData(); + + $form = $this->createForm(ForumWebhookType::class, $data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $webhook = $data->toWebhook($forum); + + $em->persist($webhook); + $em->flush(); + + $this->addFlash('success', 'flash.webhook_added'); + + return $this->redirectToRoute('forum_webhooks', [ + 'forum_name' => $forum->getName(), + ]); + } + + return $this->render('forum/add_webhook.html.twig', [ + 'form' => $form->createView(), + 'forum' => $forum, + ]); + } + + /** + * @Entity("webhook", expr="repository.findOneBy({forum: forum, id: webhook_id})") + * @IsGranted("moderator", subject="forum") + * + * @param Forum $forum + * @param ForumWebhook $webhook + * @param Request $request + * @param EntityManager $em + * + * @return Response + */ + public function editWebhook(Forum $forum, ForumWebhook $webhook, Request $request, EntityManager $em) { + if (!$this->enableWebhooks) { + throw $this->createNotFoundException('Webhooks are not enabled'); + } + + $data = new ForumWebhookData($webhook); + + $form = $this->createForm(ForumWebhookType::class, $data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data->updateWebhook($webhook); + + $em->flush(); + + $this->addFlash('success', 'flash.webhook_edited'); + + return $this->redirectToRoute('forum_webhooks', [ + 'forum_name' => $forum->getName(), + ]); + } + + return $this->render('forum/edit_webhook.html.twig', [ + 'form' => $form->createView(), + 'forum' => $forum, + ]); + } + + /** + * @IsGranted("moderator", subject="forum") + * + * @param Forum $forum + * @param Request $request + * @param ForumWebhookRepository $repository + * @param EntityManager $em + * + * @return Response + */ + public function removeWebhook(Forum $forum, Request $request, ForumWebhookRepository $repository, EntityManager $em) { + if (!$this->enableWebhooks) { + throw $this->createNotFoundException('Webhooks are not enabled'); + } + + $this->validateCsrf('remove_webhook', $request->request->get('token')); + + $ids = (array) $request->request->get('webhook'); + $ids = \array_filter($ids, function ($id) { + return \is_string($id) && Uuid::isValid($id); + }); + + $webhooks = $repository->findBy(['id' => $ids, 'forum' => $forum]); + + foreach ($webhooks as $webhook) { + $em->remove($webhook); + } + + $em->flush(); + + return $this->redirectToRoute('forum_webhooks', [ + 'forum_name' => $forum->getName(), + ]); + } + /** * @IsGranted("moderator", subject="forum") * diff --git a/src/Controller/SubmissionController.php b/src/Controller/SubmissionController.php index eff0fc7..d4a190c 100644 --- a/src/Controller/SubmissionController.php +++ b/src/Controller/SubmissionController.php @@ -7,6 +7,8 @@ use App\Entity\ForumLogSubmissionDeletion; use App\Entity\ForumLogSubmissionLock; use App\Entity\Submission; +use App\Event\EntityModifiedEvent; +use App\Events; use App\Form\DeleteReasonType; use App\Form\Model\SubmissionData; use App\Form\SubmissionType; @@ -14,6 +16,8 @@ use Doctrine\ORM\EntityManager; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -79,13 +83,19 @@ public function shortcut(Submission $submission) { * * @IsGranted("ROLE_USER") * - * @param EntityManager $em - * @param Request $request - * @param Forum $forum + * @param Forum|null $forum + * @param EntityManager $em + * @param Request $request + * @param EventDispatcherInterface $dispatcher * * @return Response */ - public function submit(EntityManager $em, Request $request, Forum $forum = null) { + public function submit( + Forum $forum = null, + EntityManager $em, + Request $request, + EventDispatcherInterface $dispatcher + ) { $data = new SubmissionData($forum); $form = $this->createForm(SubmissionType::class, $data); @@ -97,6 +107,8 @@ public function submit(EntityManager $em, Request $request, Forum $forum = null) $em->persist($submission); $em->flush(); + $dispatcher->dispatch(Events::NEW_SUBMISSION, new GenericEvent($submission)); + return $this->redirectToRoute('submission', [ 'forum_name' => $submission->getForum()->getName(), 'submission_id' => $submission->getId(), @@ -113,26 +125,37 @@ public function submit(EntityManager $em, Request $request, Forum $forum = null) /** * @IsGranted("edit", subject="submission") * - * @param EntityManager $em - * @param Forum $forum - * @param Submission $submission - * @param Request $request + * @param Forum $forum + * @param Submission $submission + * @param EntityManager $em + * @param Request $request + * @param EventDispatcherInterface $dispatcher * * @return Response */ - public function editSubmission(EntityManager $em, Forum $forum, Submission $submission, Request $request) { + public function editSubmission( + Forum $forum, + Submission $submission, + EntityManager $em, + Request $request, + EventDispatcherInterface $dispatcher + ) { $data = SubmissionData::createFromSubmission($submission); $form = $this->createForm(SubmissionType::class, $data); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $before = clone $submission; $data->updateSubmission($submission); $em->flush(); $this->addFlash('notice', 'flash.submission_edited'); + $event = new EntityModifiedEvent($before, $submission); + $dispatcher->dispatch(Events::EDIT_SUBMISSION, $event); + return $this->redirectToRoute('submission', [ 'forum_name' => $forum->getName(), 'submission_id' => $submission->getId(), diff --git a/src/Entity/Forum.php b/src/Entity/Forum.php index 679dd81..230aa67 100644 --- a/src/Entity/Forum.php +++ b/src/Entity/Forum.php @@ -130,6 +130,13 @@ class Forum { */ private $logEntries; + /** + * @ORM\OneToMany(targetEntity="ForumWebhook", mappedBy="forum") + * + * @var ForumWebhook[]|Collection + */ + private $webhooks; + public function __construct( string $name, string $title, @@ -148,6 +155,7 @@ public function __construct( $this->submissions = new ArrayCollection(); $this->subscriptions = new ArrayCollection(); $this->logEntries = new ArrayCollection(); + $this->webhooks = new ArrayCollection(); if ($user) { $this->addModerator(new Moderator($this, $user)); @@ -390,6 +398,25 @@ public function addLogEntry(ForumLogEntry $entry) { } } + /** + * @return ForumWebhook[]|Collection + */ + public function getWebhooks(): Collection { + return $this->webhooks; + } + + /** + * @param string $event + * + * @return ForumWebhook[] + */ + public function getWebhooksByEvent(string $event): array { + $criteria = Criteria::create() + ->where(Criteria::expr()->eq('event', $event)); + + return $this->webhooks->matching($criteria)->toArray(); + } + public static function normalizeName(string $name): string { return mb_strtolower($name, 'UTF-8'); } diff --git a/src/Entity/ForumWebhook.php b/src/Entity/ForumWebhook.php new file mode 100644 index 0000000..7fddce5 --- /dev/null +++ b/src/Entity/ForumWebhook.php @@ -0,0 +1,105 @@ +id = Uuid::uuid4(); + $this->forum = $forum; + $this->setEvent($event); + $this->url = $url; + $this->secretToken = $secretToken; + } + + public function getId(): Uuid { + return $this->id; + } + + public function getForum(): Forum { + return $this->forum; + } + + public function getEvent(): string { + return $this->event; + } + + public function setEvent(string $event): void { + if (!\in_array($event, self::EVENTS, true)) { + throw new \DomainException(\sprintf('Invalid event "%s"', $event)); + } + + $this->event = $event; + } + + public function getUrl(): string { + return $this->url; + } + + public function setUrl(string $url): void { + $this->url = $url; + } + + public function getSecretToken(): ?string { + return $this->secretToken; + } + + public function setSecretToken(?string $secretToken): void { + $this->secretToken = $secretToken; + } +} diff --git a/src/Entity/UserFlags.php b/src/Entity/UserFlags.php index 105d270..5c8679b 100644 --- a/src/Entity/UserFlags.php +++ b/src/Entity/UserFlags.php @@ -5,15 +5,30 @@ /** * Flags that apply to a submission or comment and which describes the role of * the poster. + * + * @todo just use strings as constants instead - needs db changes, etc. */ final class UserFlags { - const FLAGS = [ + public const FLAGS = [ self::FLAG_NONE, self::FLAG_MODERATOR, self::FLAG_ADMIN, ]; - const FLAG_NONE = 0; - const FLAG_MODERATOR = 1; - const FLAG_ADMIN = 2; + public const FLAG_NONE = 0; + public const FLAG_MODERATOR = 1; + public const FLAG_ADMIN = 2; + + public static function toReadable(int $userFlag): ?string { + switch ($userFlag) { + case self::FLAG_NONE: + return null; + case self::FLAG_MODERATOR: + return 'moderator'; + case self::FLAG_ADMIN: + return 'admin'; + default: + throw new \InvalidArgumentException(); + } + } } diff --git a/src/Event/EntityModifiedEvent.php b/src/Event/EntityModifiedEvent.php new file mode 100644 index 0000000..f3ab753 --- /dev/null +++ b/src/Event/EntityModifiedEvent.php @@ -0,0 +1,27 @@ +before = $before; + $this->after = $after; + } + + public function getBefore() { + return $this->before; + } + + public function getAfter() { + return $this->after; + } +} diff --git a/src/EventListener/WebhookListener.php b/src/EventListener/WebhookListener.php new file mode 100644 index 0000000..256230c --- /dev/null +++ b/src/EventListener/WebhookListener.php @@ -0,0 +1,184 @@ +client = $client; + $this->requestStack = $requestStack; + $this->serializer = $serializer; + $this->webhooksEnabled = $webhooksEnabled; + $this->logger = $logger ?: new NullLogger(); + } + + public function onKernelTerminate(PostResponseEvent $event): void { + if ( + !$this->webhooksEnabled || + $event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST || + !$event->getRequest()->attributes->has(self::QUEUE_KEY) + ) { + return; + } + + // request not available from stack after kernel.terminate + $queue = $this->getQueue($event->getRequest()); + + if ($queue->isEmpty()) { + return; + } + + $requests = function () use ($queue) { + while (!$queue->isEmpty()) { + $item = $queue->pop(); + + /* @var ForumWebhook $webhook */ + $webhook = $item['webhook']; + + if ($webhook->getSecretToken() !== null) { + $headers['X-Postmill-Secret'] = $webhook->getSecretToken(); + } + + $body = $this->serializer->serialize([ + 'event' => $webhook->getEvent(), + 'subject' => $item['subject'], + ], 'json'); + + yield new GuzzleRequest('POST', $webhook->getUrl(), $headers ?? [], $body); + } + }; + + $pool = new Pool($this->client, $requests(), [ + 'concurrency' => 5, + 'rejected' => function ($reason) { + // todo - log so forum mods can see this + $this->logger->warning('Webhook failed', [ + 'reason' => $reason, + ]); + }, + ]); + + $pool->promise()->wait(); + } + + public function onNewSubmission(GenericEvent $event): void { + /* @var Submission $subject */ + $subject = $event->getSubject(); + $forum = $subject->getForum(); + + $this->addToQueue($forum, ForumWebhook::EVENT_NEW_SUBMISSION, $subject); + } + + public function onEditSubmission(EntityModifiedEvent $event): void { + /* @var Submission $subject */ + $subject = $event->getAfter(); + $forum = $subject->getForum(); + + $this->addToQueue($forum, ForumWebhook::EVENT_EDIT_SUBMISSION, [ + 'before' => $event->getBefore(), + 'after' => $event->getAfter(), + ]); + } + + public function onNewComment(GenericEvent $event): void { + /* @var Comment $subject */ + $subject = $event->getSubject(); + $forum = $subject->getSubmission()->getForum(); + + $this->addToQueue($forum, ForumWebhook::EVENT_NEW_COMMENT, $subject); + } + + public function onEditComment(EntityModifiedEvent $event): void { + /* @var Comment $subject */ + $subject = $event->getAfter(); + $forum = $subject->getSubmission()->getForum(); + + $this->addToQueue($forum, ForumWebhook::EVENT_EDIT_COMMENT, [ + 'before' => $event->getBefore(), + 'after' => $event->getAfter(), + ]); + } + + private function getQueue(Request $request = null): \SplStack { + $attributes = ($request ?: $this->requestStack->getMasterRequest())->attributes; + + $queue = $attributes->get(self::QUEUE_KEY, new \SplStack()); + + if (!$attributes->has(self::QUEUE_KEY)) { + $attributes->set(self::QUEUE_KEY, $queue); + } + + return $queue; + } + + private function addToQueue(Forum $forum, string $eventName, $subject): void { + $webhooks = $forum->getWebhooksByEvent($eventName); + $queue = $this->getQueue(); + + foreach ($webhooks as $webhook) { + $queue->push(['webhook' => $webhook, 'subject' => $subject]); + } + } + + public static function getSubscribedEvents(): array { + return [ + Events::NEW_SUBMISSION => 'onNewSubmission', + Events::EDIT_SUBMISSION => 'onEditSubmission', + Events::NEW_COMMENT => 'onNewComment', + Events::EDIT_COMMENT => 'onEditComment', + KernelEvents::TERMINATE => 'onKernelTerminate', + ]; + } +} diff --git a/src/Events.php b/src/Events.php new file mode 100644 index 0000000..1637112 --- /dev/null +++ b/src/Events.php @@ -0,0 +1,12 @@ +getData() && $builder->getData()->getEntityId(); + + $builder + ->add('event', ChoiceType::class, [ + 'choices' => ForumWebhook::EVENTS, + 'choice_label' => function ($key) { + return 'label.event.'.$key; + }, + 'label' => 'label.event', + 'placeholder' => $editing ? null : 'placeholder.choose_one', + ]) + ->add('url', UrlType::class, [ + 'label' => 'label.url', + ]) + ->add('secretToken', TextareaType::class, [ + 'label' => 'label.secret_token', + 'required' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => $editing ? 'action.save' : 'action.add', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) { + $resolver->setDefaults([ + 'data_class' => ForumWebhookData::class, + ]); + } +} diff --git a/src/Form/Model/ForumWebhookData.php b/src/Form/Model/ForumWebhookData.php new file mode 100644 index 0000000..0d6d601 --- /dev/null +++ b/src/Form/Model/ForumWebhookData.php @@ -0,0 +1,65 @@ +entityId = $forumWebhook->getId(); + $this->event = $forumWebhook->getEvent(); + $this->url = $forumWebhook->getUrl(); + $this->secretToken = $forumWebhook->getSecretToken(); + } + } + + public function toWebhook(Forum $forum): ForumWebhook { + return new ForumWebhook( + $forum, + $this->event, + $this->url, + $this->secretToken + ); + } + + public function updateWebhook(ForumWebhook $forumWebhook): void { + $forumWebhook->setEvent($this->event); + $forumWebhook->setUrl($this->url); + $forumWebhook->setSecretToken($this->secretToken); + } + + public function getEntityId(): ?Uuid { + return $this->entityId; + } +} diff --git a/src/Migrations/Version20180325060258.php b/src/Migrations/Version20180325060258.php new file mode 100644 index 0000000..4ae24db --- /dev/null +++ b/src/Migrations/Version20180325060258.php @@ -0,0 +1,23 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE TABLE forum_webhooks (id UUID NOT NULL, forum_id BIGINT NOT NULL, event TEXT NOT NULL, url TEXT NOT NULL, secret_token TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BE7FC23A29CCBAD0 ON forum_webhooks (forum_id)'); + $this->addSql('COMMENT ON COLUMN forum_webhooks.id IS \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE forum_webhooks ADD CONSTRAINT FK_BE7FC23A29CCBAD0 FOREIGN KEY (forum_id) REFERENCES forums (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema) { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP TABLE forum_webhooks'); + } +} diff --git a/src/Repository/ForumWebhookRepository.php b/src/Repository/ForumWebhookRepository.php new file mode 100644 index 0000000..58f83c4 --- /dev/null +++ b/src/Repository/ForumWebhookRepository.php @@ -0,0 +1,14 @@ +urlGenerator = $urlGenerator; + } + + public function denormalize($data, $class, $format = null, array $context = []) { + // TODO: Implement denormalize() method. + } + + public function supportsDenormalization($data, $type, $format = null): bool { + return false; + } + + public function normalize($object, $format = null, array $context = []): array { + if (!$object instanceof Comment) { + throw new \InvalidArgumentException(); + } + + $normalized = [ + 'resource' => $this->urlGenerator->generate('comment', [ + 'forum_name' => $object->getSubmission()->getForum()->getName(), + 'submission_id' => $object->getSubmission()->getId(), + 'comment_id' => $object->getId(), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'id' => $object->getId(), + 'body' => $object->getBody(), + 'timestamp' => $object->getTimestamp()->format('c'), + 'user' => $this->urlGenerator->generate('user', [ + 'username' => $object->getUser()->getUsername(), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'submission' => $this->urlGenerator->generate('submission', [ + 'forum_name' => $object->getSubmission()->getForum()->getName(), + 'submission_id' => $object->getSubmission()->getId(), + 'slug' => Slugger::slugify($object->getSubmission()->getTitle()), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'parent' => $object->getParent() + ? $this->urlGenerator->generate('comment', [ + 'forum_name' => $object->getSubmission()->getForum()->getName(), + 'submission_id' => $object->getSubmission()->getId(), + 'comment_id' => $object->getParent()->getId(), + ], UrlGeneratorInterface::ABSOLUTE_URL) + : null, + 'reply_count' => \count($object->getChildren()), + 'upvotes' => $object->getUpvotes(), + 'downvotes' => $object->getDownvotes(), + 'soft_deleted' => $object->isSoftDeleted(), + 'edited_at' => $object->getEditedAt() + ? $object->getEditedAt()->format('c') + : null, + 'moderated' => $object->isModerated(), + 'user_flag' => UserFlags::toReadable($object->getUserFlag()), + ]; + + return \array_filter($normalized, function ($element) { + return $element !== null; + }); + } + + public function supportsNormalization($data, $format = null): bool { + return $data instanceof Comment; + } +} diff --git a/src/Serializer/SubmissionNormalizer.php b/src/Serializer/SubmissionNormalizer.php new file mode 100644 index 0000000..af01062 --- /dev/null +++ b/src/Serializer/SubmissionNormalizer.php @@ -0,0 +1,107 @@ +urlGenerator = $urlGenerator; + $this->cacheManager = $liipCacheManager; + $this->requestStack = $requestStack; + } + + public function denormalize($data, $class, $format = null, array $context = []) { + // TODO + } + + public function supportsDenormalization($data, $type, $format = null): bool { + return false; + } + + public function normalize($object, $format = null, array $context = []): array { + if (!$object instanceof Submission) { + throw new \InvalidArgumentException(); + } + + $normalized = [ + 'resource' => $this->urlGenerator->generate('submission', [ + 'forum_name' => $object->getForum()->getName(), + 'submission_id' => $object->getId(), + 'slug' => Slugger::slugify($object->getTitle()), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'id' => $object->getId(), + 'forum' => $this->urlGenerator->generate('forum', [ + 'forum_name' => $object->getForum()->getName(), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'user' => $this->urlGenerator->generate('user', [ + 'username' => $object->getUser()->getUsername(), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'title' => $object->getTitle(), + 'body' => $object->getBody(), + 'url' => $object->getUrl(), + 'timestamp' => $object->getTimestamp()->format('c'), + 'locked' => $object->isLocked(), + 'sticky' => $object->isSticky(), + 'user_flag' => UserFlags::toReadable($object->getUserFlag()), + 'edited_at' => $object->getEditedAt() + ? $object->getEditedAt()->format('c') + : null, + 'moderated' => $object->isModerated(), + 'comment_count' => \count($object->getComments()), + 'upvotes' => $object->getUpvotes(), + 'downvotes' => $object->getDownvotes(), + ]; + + if ($object->getImage()) { + $normalized['thumbnail_1x'] = $this->cacheManager->generateUrl( + $object->getImage(), + 'submission_thumbnail_1x' + ); + + $normalized['thumbnail_2x'] = $this->cacheManager->generateUrl( + $object->getImage(), + 'submission_thumbnail_2x' + ); + } + + return \array_filter($normalized, function ($element) { + return $element !== null; + }); + } + + public function supportsNormalization($data, $format = null): bool { + return $data instanceof Submission; + } +} diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 0c94230..a8cfdc9 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -25,10 +25,16 @@ final class AppExtension extends AbstractExtension { */ private $version; - public function __construct(string $siteName, ?string $branch, ?string $version) { + /** + * @var bool + */ + private $enableWebhooks; + + public function __construct(string $siteName, ?string $branch, ?string $version, bool $enableWebhooks) { $this->siteName = $siteName; $this->branch = $branch; $this->version = $version; + $this->enableWebhooks = $enableWebhooks; } public function getFunctions(): array { @@ -42,6 +48,9 @@ public function getFunctions(): array { new TwigFunction('app_version', function () { return $this->version; }), + new TwigFunction('app_webhooks_enabled', function () { + return $this->enableWebhooks; + }), ]; } } diff --git a/symfony.lock b/symfony.lock index 4a238af..c38bfba 100644 --- a/symfony.lock +++ b/symfony.lock @@ -89,6 +89,15 @@ "egulias/email-validator": { "version": "2.1.3" }, + "eightpoints/guzzle-bundle": { + "version": "7.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "7.0", + "ref": "8be83603226d268ee8a93721b5bb77ec71d8d334" + } + }, "embed/embed": { "version": "v3.2.2" }, @@ -122,6 +131,15 @@ "gregwar/captcha-bundle": { "version": "dev-master" }, + "guzzlehttp/guzzle": { + "version": "6.3.0" + }, + "guzzlehttp/promises": { + "version": "v1.3.1" + }, + "guzzlehttp/psr7": { + "version": "1.4.2" + }, "imagine/imagine": { "version": "v0.6.3" }, @@ -176,6 +194,9 @@ "psr/container": { "version": "1.0.0" }, + "psr/http-message": { + "version": "1.0.1" + }, "psr/link": { "version": "1.0.0" }, diff --git a/templates/forum/_webhook_form.html.twig b/templates/forum/_webhook_form.html.twig new file mode 100644 index 0000000..d9a9b16 --- /dev/null +++ b/templates/forum/_webhook_form.html.twig @@ -0,0 +1,9 @@ +{{ form_start(form) }} + {{ form_row(form.event) }} + {{ form_row(form.url) }} + {{ form_row(form.secretToken) }} + +
+ {{ form_widget(form.submit) }} +
+{{ form_end(form) }} diff --git a/templates/forum/add_webhook.html.twig b/templates/forum/add_webhook.html.twig new file mode 100644 index 0000000..ac42e75 --- /dev/null +++ b/templates/forum/add_webhook.html.twig @@ -0,0 +1,15 @@ +{% extends 'forum/base.html.twig' %} + +{% block page_classes 'add-forum-webhook-page' %} +{% block title 'title.add_forum_webhook'|trans({'%forum%': '/f/'~forum.name}) %} + +{% block body %} +

+ {{ 'title.add_forum_webhook'|trans({'%forum%': '/f/%s'|format( + path('forum', {forum_name: forum.name})|e, + forum.name|e + ) })|raw }} +

+ + {{ include('forum/_webhook_form.html.twig', {form: form}, with_context=false) }} +{% endblock %} diff --git a/templates/forum/base.html.twig b/templates/forum/base.html.twig index fa0b83f..94f7571 100644 --- a/templates/forum/base.html.twig +++ b/templates/forum/base.html.twig @@ -64,6 +64,13 @@
  • {{ 'nav.appearance'|trans }}
  • {% endset %} {% set items = items|merge({('nav.appearance'|trans): item}) %} + + {% if app_webhooks_enabled() %} + {% set item %} +
  • {{ 'nav.webhooks'|trans }}
  • + {% endset %} + {% set items = items|merge({('nav.webhooks'|trans): item}) %} + {% endif %} {% endif %} {% if is_granted('ROLE_ADMIN') %} diff --git a/templates/forum/edit_webhook.html.twig b/templates/forum/edit_webhook.html.twig new file mode 100644 index 0000000..41fba84 --- /dev/null +++ b/templates/forum/edit_webhook.html.twig @@ -0,0 +1,17 @@ +{% extends 'forum/base.html.twig' %} + +{% block page_classes 'edit-forum-webhook-page' %} +{% block title 'title.editing_forum_webhook'|trans({'%forum%': '/f/'~forum.name}) %} + +{% block body %} +

    + {{ 'title.editing_forum_webhook'|trans({ + '%forum%': '%s'|format( + path('forum', {forum_name: forum.name})|e, + '/f/'~forum.name|e + )})|raw }} +

    + + {{ include('forum/_webhook_form.html.twig', {form: form}, with_context=false) }} +{% endblock %} + diff --git a/templates/forum/webhooks.html.twig b/templates/forum/webhooks.html.twig new file mode 100644 index 0000000..a5a2f3b --- /dev/null +++ b/templates/forum/webhooks.html.twig @@ -0,0 +1,57 @@ +{% extends 'forum/base.html.twig' %} + +{% block page_classes 'forum-webhooks-page' %} +{% block title 'title.forum_webhooks'|trans({'%forum%': '/f/'~forum.name}) %} + +{% block body %} +

    + {{ 'title.forum_webhooks'|trans({'%forum%': '/f/%s'|format( + path('forum', {forum_name: forum.name})|e, + forum.name|e + ) })|raw }} +

    + +

    + + {{ 'nav.add_webhook'|trans }} + +

    + + {% if forum.webhooks|length > 0 %} +
    + + + + + + + + + + + + {% for webhook in forum.webhooks %} + + + + + + + {% endfor %} + +
    {{ 'label.event'|trans }}{{ 'label.url'|trans }}
    {{ ('label.event.'~webhook.event)|trans }}{{ webhook.url }} + {{ 'action.edit'|trans }} +
    + +
    + +
    +
    + {% else %} +
    +

    {{ 'flash.no_entries_to_display'|trans }}

    +
    + {% endif %} +{% endblock %} + diff --git a/tests/Event/EntityModifiedEventTest.php b/tests/Event/EntityModifiedEventTest.php new file mode 100644 index 0000000..1d44474 --- /dev/null +++ b/tests/Event/EntityModifiedEventTest.php @@ -0,0 +1,22 @@ + Date: Sat, 31 Mar 2018 06:39:28 +0200 Subject: [PATCH 05/24] add webhook documentation to index --- docs/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.md b/docs/index.md index 3158ade..a73d0b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,10 @@ * **[Getting started](../README.md#getting-started)** +* **Reference** + + * [Webhooks](webhooks.md) + * **Configuration & administration** * [Database setup](database-setup.md) From 495261a813e482f804cb6bfeb924c95b3d2983ab Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 31 Mar 2018 20:13:09 +0200 Subject: [PATCH 06/24] refactor SubmissionImageListener to use guzzle --- composer.json | 1 - composer.lock | 3 +- config/packages/eight_points_guzzle.yaml | 7 + config/services.yaml | 5 +- src/EventListener/SubmissionImageListener.php | 142 ++++++++++-------- 5 files changed, 87 insertions(+), 71 deletions(-) diff --git a/composer.json b/composer.json index d882b8c..82c7651 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "require": { "php": "^7.1.3", "ext-apcu": "*", - "ext-curl": "*", "ext-iconv": "*", "ext-pdo_pgsql": "*", "doctrine/doctrine-migrations-bundle": "^1.2", diff --git a/composer.lock b/composer.lock index ad67ac0..16a5bb2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "05cf9ecf6e555b98c3adb6f3f8d9ced3", + "content-hash": "0d8eeac6d40a570be8925fb7940fdc02", "packages": [ { "name": "composer/ca-bundle", @@ -7529,7 +7529,6 @@ "platform": { "php": "^7.1.3", "ext-apcu": "*", - "ext-curl": "*", "ext-iconv": "*", "ext-pdo_pgsql": "*" }, diff --git a/config/packages/eight_points_guzzle.yaml b/config/packages/eight_points_guzzle.yaml index 30f9549..d3705f9 100644 --- a/config/packages/eight_points_guzzle.yaml +++ b/config/packages/eight_points_guzzle.yaml @@ -14,3 +14,10 @@ eight_points_guzzle: # Find plugins here: # https://github.com/8p/EightPointsGuzzleBundle#known-and-supported-plugins plugin: ~ + + submission_image_client: + options: + timeout: 10 + headers: + User-Agent: "Postmill/0.6 (https://postmill.xyz/)" + Accept: image/png, image/jpeg, image/gif diff --git a/config/services.yaml b/config/services.yaml index 13af52b..7a07afb 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -62,11 +62,8 @@ services: App\EventListener\SubmissionImageListener: arguments: + $client: "@eight_points_guzzle.client.submission_image_client" $filesystem: "@oneup_flysystem.submission_images_filesystem" - public: true - tags: - - { name: doctrine.event_listener, event: postPersist } - - { name: kernel.event_listener, event: kernel.terminate } App\EventListener\WebhookListener: arguments: diff --git a/src/EventListener/SubmissionImageListener.php b/src/EventListener/SubmissionImageListener.php index 352d8f2..b983518 100644 --- a/src/EventListener/SubmissionImageListener.php +++ b/src/EventListener/SubmissionImageListener.php @@ -3,19 +3,23 @@ namespace App\EventListener; use App\Entity\Submission; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\Event\LifecycleEventArgs; +use App\Events; +use Doctrine\ORM\EntityManagerInterface; use Embed\Embed; use Embed\Exceptions\EmbedException; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use League\Flysystem\FileExistsException; use League\Flysystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpKernel\Event\KernelEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -23,13 +27,18 @@ /** * Download related image after submission. */ -final class SubmissionImageListener { +final class SubmissionImageListener implements EventSubscriberInterface { const QUEUE_KEY = 'submission_thumbnail_queue'; /** - * @var ManagerRegistry + * @var Client */ - private $registry; + private $client; + + /** + * @var EntityManagerInterface + */ + private $entityManager; /** * @var FilesystemInterface @@ -46,16 +55,21 @@ final class SubmissionImageListener { */ private $validator; + /** + * @var LoggerInterface + */ + private $logger; + public function __construct( - ManagerRegistry $registry, + Client $client, + EntityManagerInterface $entityManager, FilesystemInterface $filesystem, RequestStack $requestStack, ValidatorInterface $validator, LoggerInterface $logger = null ) { - // we must inject the registry rather than the manager because of a - // nasty infinite loop bug that would occur in the container - $this->registry = $registry; + $this->client = $client; + $this->entityManager = $entityManager; $this->filesystem = $filesystem; $this->requestStack = $requestStack; $this->validator = $validator; @@ -65,18 +79,20 @@ public function __construct( /** * Stick every submission with a URL in a queue. * - * @param LifecycleEventArgs $args + * @param GenericEvent $event */ - public function postPersist(LifecycleEventArgs $args) { + public function onNewSubmission(GenericEvent $event) { $request = $this->requestStack->getMasterRequest(); - $entity = $args->getEntity(); - if (!$request || !$entity instanceof Submission || !$entity->getUrl() || $entity->getImage()) { + /* @var Submission $submission */ + $submission = $event->getSubject(); + + if (!$request || !$submission->getUrl() || $submission->getImage()) { return; } $queue = $request->attributes->get(self::QUEUE_KEY, []); - $queue[] = $entity; + $queue[] = $submission; $request->attributes->set(self::QUEUE_KEY, $queue); } @@ -84,20 +100,16 @@ public function postPersist(LifecycleEventArgs $args) { /** * Loop through the queue at the end of the request and download the images. * - * @param KernelEvent $event + * @param PostResponseEvent $event */ - public function onKernelTerminate(KernelEvent $event) { - if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) { - return; - } - + public function onKernelTerminate(PostResponseEvent $event) { $queue = $event->getRequest()->attributes->get(self::QUEUE_KEY, []); if (!$queue) { return; } - /** @var Submission $submission */ + /* @var Submission $submission */ foreach ($queue as $submission) { try { $embed = Embed::create($submission->getUrl()); @@ -111,7 +123,7 @@ public function onKernelTerminate(KernelEvent $event) { } } - $this->registry->getManager()->flush(); + $this->entityManager->flush(); } /** @@ -121,45 +133,21 @@ public function onKernelTerminate(KernelEvent $event) { * * @return string|null the final file name, or null if the download failed */ - private function getFilename(string $imageUrl) { - error_clear_last(); + private function getFilename(string $imageUrl): ?string { + $oldExceptionHandler = \set_error_handler(__NAMESPACE__.'\error_handler'); try { - // fixme: don't create temporary files - $tempFile = @tempnam(sys_get_temp_dir(), 'postmill'); - $fh = @fopen($tempFile, 'wb+'); - - if (!$fh) { - $this->logger->warning('Could not open file for writing', [ - 'error' => error_get_last(), - ]); - - return null; - } + $tempFile = \tempnam(\sys_get_temp_dir(), 'pml'); - // todo: refactor to use guzzle or something - $ch = curl_init($imageUrl); - curl_setopt($ch, CURLOPT_FILE, $fh); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, 5); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $this->client->get($imageUrl, ['sink' => $tempFile]); - $success = curl_exec($ch) && curl_getinfo($ch, CURLINFO_RESPONSE_CODE) == 200; - - if (!$success) { - $this->logger->info('Bad HTTP response', [ - 'curl' => curl_getinfo($ch), - ]); - - return null; - } - - $imageConstraint = new Image(['detectCorrupted' => true]); - - $violations = $this->validator->validate($tempFile, $imageConstraint); + $violations = $this->validator->validate( + $tempFile, + new Image(['detectCorrupted' => true]) + ); if (count($violations) > 0) { - /** @var ConstraintViolationInterface $violation */ + /* @var ConstraintViolationInterface $violation */ foreach ($violations as $violation) { $message = $violation->getMessageTemplate(); $params = $violation->getParameters(); @@ -176,19 +164,45 @@ private function getFilename(string $imageUrl) { $filename = sprintf('%s.%s', hash_file('sha256', $tempFile), $ext); try { + $fh = fopen($tempFile, 'rb'); $success = $this->filesystem->writeStream($filename, $fh); } catch (FileExistsException $e) { $success = true; } - return $success ? $filename : null; - } finally { - if (isset($ch)) { - @curl_close($ch); + if ($success) { + return $filename; } - - @fclose($fh); - @unlink($tempFile); } + catch (GuzzleException $e) { + $this->logger->notice('Failed to download submission image', [ + 'exception' => $e, + ]); + } catch (\ErrorException $e) { + $this->logger->warning($e->getMessage(), [ + 'exception' => $e, + ]); + } finally { + \set_exception_handler($oldExceptionHandler); + @\unlink($tempFile); + @\fclose($fh); + } + + return null; + } + + public static function getSubscribedEvents() { + return [ + Events::NEW_SUBMISSION => 'onNewSubmission', + KernelEvents::TERMINATE => 'onKernelTerminate', + ]; } } + +function error_handler($severity, $message, $file, $line) { + if (!(error_reporting() & $severity)) { + return; + } + + throw new \ErrorException($message, 0, $severity, $file, $line); +} From b12fecc2e14f561643b9c476a8e247bf48cb3a2f Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 31 Mar 2018 23:54:22 +0200 Subject: [PATCH 07/24] factor EntityEditListener away --- config/services.yaml | 4 -- src/Controller/CommentController.php | 2 +- src/Controller/SubmissionController.php | 2 +- src/EventListener/EntityEditListener.php | 74 ------------------------ src/Form/Model/CommentData.php | 12 +++- src/Form/Model/SubmissionData.php | 20 +++++-- tests/Form/Model/CommentDataTest.php | 54 +++++++++++++++++ tests/Form/Model/SubmissionDataTest.php | 66 +++++++++++++++++++++ 8 files changed, 148 insertions(+), 86 deletions(-) delete mode 100644 src/EventListener/EntityEditListener.php create mode 100644 tests/Form/Model/CommentDataTest.php create mode 100644 tests/Form/Model/SubmissionDataTest.php diff --git a/config/services.yaml b/config/services.yaml index 7a07afb..9d66a0b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -13,10 +13,6 @@ services: autowire: true public: false - _instanceof: - Doctrine\Common\EventSubscriber: - tags: ['doctrine.event_subscriber'] - # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: diff --git a/src/Controller/CommentController.php b/src/Controller/CommentController.php index b51b54c..f728460 100644 --- a/src/Controller/CommentController.php +++ b/src/Controller/CommentController.php @@ -153,7 +153,7 @@ public function editComment( if ($form->isSubmitted() && $form->isValid()) { $before = clone $comment; - $data->updateComment($comment); + $data->updateComment($comment, $this->getUser()); $em->flush(); diff --git a/src/Controller/SubmissionController.php b/src/Controller/SubmissionController.php index d4a190c..039e027 100644 --- a/src/Controller/SubmissionController.php +++ b/src/Controller/SubmissionController.php @@ -147,7 +147,7 @@ public function editSubmission( if ($form->isSubmitted() && $form->isValid()) { $before = clone $submission; - $data->updateSubmission($submission); + $data->updateSubmission($submission, $this->getUser()); $em->flush(); diff --git a/src/EventListener/EntityEditListener.php b/src/EventListener/EntityEditListener.php deleted file mode 100644 index 7abf731..0000000 --- a/src/EventListener/EntityEditListener.php +++ /dev/null @@ -1,74 +0,0 @@ -tokenStorage = $tokenStorage; - } - - /** - * @param PreUpdateEventArgs $args - */ - public function preUpdate(PreUpdateEventArgs $args) { - $entity = $args->getEntity(); - - if ( - $entity instanceof Comment && $this->commentIsEdited($args) || - $entity instanceof Submission && $this->submissionIsEdited($args) - ) { - $entity->setEditedAt(new \DateTime('@'.time())); - - if ($this->isModerated($args->getEntity())) { - $entity->setModerated(true); - } - } - } - - private function commentIsEdited(PreUpdateEventArgs $args): bool { - return $args->hasChangedField('body'); - } - - private function submissionIsEdited(PreUpdateEventArgs $args): bool { - return $args->hasChangedField('body') || - $args->hasChangedField('title') || - $args->hasChangedField('url'); - } - - /** - * @param object $entity - * - * @return bool - */ - private function isModerated($entity): bool { - $token = $this->tokenStorage->getToken(); - - return $token && - $token->getUser() instanceof User && - $token->getUser() !== $entity->getUser(); - } - - /** - * {@inheritdoc} - */ - public function getSubscribedEvents() { - return ['preUpdate']; - } -} diff --git a/src/Form/Model/CommentData.php b/src/Form/Model/CommentData.php index 8e00b59..3033a92 100644 --- a/src/Form/Model/CommentData.php +++ b/src/Form/Model/CommentData.php @@ -47,9 +47,17 @@ public function toComment( ); } - public function updateComment(Comment $comment) { - $comment->setBody($this->body); + public function updateComment(Comment $comment, User $editingUser) { $comment->setUserFlag($this->userFlag); + + if ($this->body !== $comment->getBody()) { + $comment->setBody($this->body); + $comment->setEditedAt(new \DateTime('@'.time())); + + if (!$comment->isModerated()) { + $comment->setModerated($comment->getUser() !== $editingUser); + } + } } /** diff --git a/src/Form/Model/SubmissionData.php b/src/Form/Model/SubmissionData.php index 590a49f..a7862c5 100644 --- a/src/Form/Model/SubmissionData.php +++ b/src/Form/Model/SubmissionData.php @@ -82,10 +82,22 @@ public function toSubmission(User $user, $ip): Submission { ); } - public function updateSubmission(Submission $submission) { - $submission->setTitle($this->title); - $submission->setUrl($this->url); - $submission->setBody($this->body); + public function updateSubmission(Submission $submission, User $editingUser) { + if ( + $this->url !== $submission->getUrl() || + $this->title !== $submission->getTitle() || + $this->body !== $submission->getBody() + ) { + $submission->setTitle($this->title); + $submission->setUrl($this->url); + $submission->setBody($this->body); + $submission->setEditedAt(new \DateTime('@'.time())); + + if (!$submission->isModerated()) { + $submission->setModerated($submission->getUser() !== $editingUser); + } + } + $submission->setUserFlag($this->userFlag); $submission->setSticky($this->sticky); } diff --git a/tests/Form/Model/CommentDataTest.php b/tests/Form/Model/CommentDataTest.php new file mode 100644 index 0000000..ce072ba --- /dev/null +++ b/tests/Form/Model/CommentDataTest.php @@ -0,0 +1,54 @@ +comment = $this->getMockBuilder(Comment::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); + + $this->comment + ->method('getUser') + ->willReturn($this->createMock(User::class)); + + $this->comment->setBody('foo'); + } + + public function testUpdate() { + $data = CommentData::createFromComment($this->comment); + $data->setBody('bar'); + $data->updateComment($this->comment, $this->comment->getUser()); + + $this->assertEquals(new \DateTime('@'.time()), $this->comment->getEditedAt()); + $this->assertFalse($this->comment->isModerated()); + + sleep(5); + + $data->setBody('baz'); + $data->updateComment($this->comment, $this->createMock(User::class)); + + $this->assertEquals(new \DateTime('@'.time()), $this->comment->getEditedAt()); + $this->assertTrue($this->comment->isModerated()); + } +} diff --git a/tests/Form/Model/SubmissionDataTest.php b/tests/Form/Model/SubmissionDataTest.php new file mode 100644 index 0000000..49d0501 --- /dev/null +++ b/tests/Form/Model/SubmissionDataTest.php @@ -0,0 +1,66 @@ +submission = $this->getMockBuilder(Submission::class) + ->disableOriginalConstructor() + ->setMethods(['getUser', 'getForum']) + ->getMock(); + + $this->submission + ->method('getUser') + ->willReturn($this->createMock(User::class)); + + $this->submission->setUserFlag(0); + $this->submission->setTitle('foo'); + $this->submission->setUrl('http://example.com/'); + $this->submission->setBody('bar'); + } + + public function testUpdate() { + $data = SubmissionData::createFromSubmission($this->submission); + $data->setBody('bleh'); + $data->updateSubmission($this->submission, $this->submission->getUser()); + + $this->assertEquals('bleh', $this->submission->getBody()); + $this->assertFalse($this->submission->isModerated()); + $this->assertEquals(new \DateTime('@'.time()), $this->submission->getEditedAt()); + + sleep(5); + + $data->setTitle('poop'); + $data->updateSubmission($this->submission, $this->createMock(User::class)); + + $this->assertTrue($this->submission->isModerated()); + $this->assertEquals(new \DateTime('@'.time()), $this->submission->getEditedAt()); + + sleep(5); + + $data->setUrl('https://example.net/a'); + $data->updateSubmission($this->submission, $this->submission->getUser()); + + $this->assertTrue($this->submission->isModerated()); + $this->assertEquals(new \DateTime('@'.time()), $this->submission->getEditedAt()); + } +} From d892901aebd23408f6e467c032bb4905274e7ece Mon Sep 17 00:00:00 2001 From: Emma Date: Sun, 1 Apr 2018 03:52:34 +0200 Subject: [PATCH 08/24] remove unnecessary service dependency --- src/Serializer/SubmissionNormalizer.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Serializer/SubmissionNormalizer.php b/src/Serializer/SubmissionNormalizer.php index af01062..fe19298 100644 --- a/src/Serializer/SubmissionNormalizer.php +++ b/src/Serializer/SubmissionNormalizer.php @@ -6,7 +6,6 @@ use App\Entity\UserFlags; use App\Utils\Slugger; use Liip\ImagineBundle\Imagine\Cache\CacheManager; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -23,15 +22,9 @@ class SubmissionNormalizer extends AbstractNormalizer { */ private $cacheManager; - /** - * @var RequestStack - */ - private $requestStack; - public function __construct( UrlGeneratorInterface $urlGenerator, CacheManager $liipCacheManager, - RequestStack $requestStack, ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null ) { @@ -39,7 +32,6 @@ public function __construct( $this->urlGenerator = $urlGenerator; $this->cacheManager = $liipCacheManager; - $this->requestStack = $requestStack; } public function denormalize($data, $class, $format = null, array $context = []) { From e80c6524c85345142eb4a8e4a58a39208e5ba5e2 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Wed, 4 Apr 2018 20:48:27 +0200 Subject: [PATCH 09/24] Updated Esperanto translation. Corrected errors, used easier words. --- translations/messages.eo.yml | 186 ++++++++++++++++----------------- translations/security.eo.yml | 1 + translations/validators.eo.yml | 4 +- 3 files changed, 96 insertions(+), 95 deletions(-) create mode 100644 translations/security.eo.yml diff --git a/translations/messages.eo.yml b/translations/messages.eo.yml index af8930e..e555de6 100644 --- a/translations/messages.eo.yml +++ b/translations/messages.eo.yml @@ -1,27 +1,27 @@ action: upvote: Porvoĉdoni downvote: Kontraŭvoĉdoni - retract_upvote: Retract porvoĉdono - retract_downvote: Retract kontraŭvoĉdono + retract_upvote: Repreni porvoĉdonon + retract_downvote: Repreni kontraŭvoĉdonon add_moderator: title: Aldoni kontrolanton al %forum% ban_add: - title: Forbari IP adreson + title: Forbari IP-adreson ban_form: - ip: IP adreso - reason: Kialo por forbari - expiry_date: Expires at (YYYY-MM-DD hh:mm) + ip: IP-adreso + reason: Kialo forbari + expiry_date: Finiĝos je (YYYY-MM-DD hh:mm) ban: Forbari user: Uzanto de IP ban_list: title: Forbaroj - ip: IP adreso + ip: IP-adreso timestamp: Tempo - banned_by: Forbaris de + banned_by: Forbarita de reason: Kialo user: Uzanto user_none: nenio @@ -31,7 +31,7 @@ ban_list: banned: title: Forbaris - message: Vi estas forbara! + message: Vi estas forbarita! comments: author_deleted: '[forigis]' @@ -40,24 +40,24 @@ comments: delete_thread: Forigi kun respondoj edit: Redakti permalink: Konstanta ligilo - parent: Gepatro + parent: Gepatra afiŝo reply: Respondi - admin_ban: IP forbaro - form_load_error: Eraro ŝarĝis formon. Bonvolu, denove provi. + admin_ban: IP-forbaro + form_load_error: Eraro pri enlego de formo. Bonvolu denove provi. info: '%user% skribis %timestamp%' info_at_timestamp: je %timestamp% not_logged_in: Vi devas %login_link% aŭ %register_link% por komenti. - not_logged_in_login_link_label: ensaluti - not_logged_in_register_link_label: registri + not_logged_in_login_link_label: saluti + not_logged_in_register_link_label: registriĝi viewing_thread: Vidi unu komentan fadenon. thread_return: Vidi ĉiujn komentojn return_to_forum: ← Reveni al %forum% - edit_info: (redaktis %edited_at%) - moderator_info: (kontrolanto redaktis %edited_at%) + edit_info: (redaktita je %edited_at%) + moderator_info: (kontrolanto redaktis je %edited_at%) comment_form: comment: Komento - submit: Afiŝis + submit: Afiŝi edit_title: Redaktas komenton create_title: Skribas komenton @@ -65,7 +65,7 @@ compose_message: title: Kreas mesaĝon por %username% create_forum: - create_new_forum: Kreas novan forumon + create_new_forum: Kreas novan diskutejon edit_forum: title: Redaktas %forum% @@ -74,46 +74,46 @@ edit_user: title: Redaktas uzanton %username% flash: - ban_added: La IP adreso estas forbara. - forum_deleted: La forumojn kaj ĉiujn enhavojn savis. - forum_updated: La redaktojn savis. - forum_moderator_added: La uzanto levigis al kontrolanto - inbox_cleared: La enirkeston vakigis. - reset_password_email_sent: Reŝargan retpoŝton sendis al specifita adreso. - submission_deleted: La sendadon savis. - submission_edited: La sendadon redaktis. - user_password_updated: Via pasvorto ĝisdatigis. + ban_added: La IP adreso estas forbarita. + forum_deleted: La diskutejo kaj ties enhavo foriĝis. + forum_updated: La redaktoj konserviĝis. + forum_moderator_added: La uzanto fariĝis kontrolanto + inbox_cleared: La enirkesto vakiĝis. + reset_password_email_sent: Restariga retletero sendiĝis al la adreso, kiun vi specifis. + submission_deleted: La afiŝo foriĝis. + submission_edited: La afiŝo redaktiĝis. + user_password_updated: Via pasvorto ĝisdatiĝis. user_settings_updated: Uzanto-agordoj ĝisdatigis. - no_messages: Sen mesaĝoj montri. + no_messages: Sen mesaĝoj montreblaj. forum: moderators: Kontrolantoj manage: Administri - edit: Redakti fourmon + edit: Redakti diskutejon subscribe: Aboni unsubscribe: Malaboni subscriber_count: '{0} Sen abonantoj|{1} %count% abonanto|[1,Inf[ %count% abonantoj' - add_moderator: Aldoni kontrolanto + add_moderator: Aldoni kontrolanton forum_form: name: Nomo title: Titolo description: Priskribo category: Kategorio - create: Krei forumon + create: Krei diskutejon save: Konservi ŝanĝojn - delete: Savi forumon - confirm_delete: Ĉu vi certe volas savi ĉi-tion forumon kaj ĉiujn komentojn kaj ĉiujn sendadojn? - featured: Malfonen porti - uncategorized_placeholder: (senkategorio) + delete: Forigi fdiskutejon + confirm_delete: Ĉu vi certe volas forigi ĉi tiun diskutejon kaj ĉiujn afiŝojn kun komentoj? + featured: Montri ĉefpaĝe + uncategorized_placeholder: (sen kategorio) forum_list: name: Nomo title: Titolo subscribers: Abonantoj - page_title: Listo de forumoj - create_forum: Krei forumon - submission_count: Afiŝonombro + page_title: Listo de diskutejoj + create_forum: Krei diskutejon + submission_count: Da afiŝoj list_view: Lista vido category_view: Kategoria vido uncategorized: Senkategoria vido @@ -122,21 +122,21 @@ forum_moderators: title: Kontrolantoj de /%forum_name% username: Uzantonomo since: Ekde - last_seen: Laste vidis + last_seen: Laste vidita front: - subscribed_forums: Abonaj forumoj - no_subscriptions: Vi ne havas neniun albonajn forumojn. Anstataŭe montras priatentitajn forumojn. - featured_forums: Priatentitaj Forumoj - no_forums: Estas neniuj priatentitaj forumoj por montri. + subscribed_forums: Abonitaj diskutejoj + no_subscriptions: Vi abonas neniun diskutejon. Anstataŭe montriĝas elstarigitaj diskutejoj. + featured_forums: Elstarigitaj diskutejoj + no_forums: Estas neniuj elstarigitaj diskutejoj montreblaj. all: Ĉiuj - featured: Priatentitaj - subscribed: Abonaj + featured: Elstarigitaj + subscribed: Abonataj inbox: title: Enirkesto - clear_inbox: Vakigi enirkeston - empty: La enirkesto estas vakiga + clear_inbox: Malplenigi enirkeston + empty: La enirkesto estas malplena. message_reply_head: 'Re: %title%' label: @@ -148,10 +148,10 @@ label: toolbox: Ilaro login_form: - log_in: Ensaluti + log_in: Saluti username: Uzantonomo password: Pasvorto - reset_password: Reŝargi pasvorton? + reset_password: Ĉu restarigi pasvorton? markdown_type: help: Sintaksa referenco @@ -164,17 +164,17 @@ message_form: moderator_form: user: Uzantonomo - submit: Aldoni tiel kontrolanto + submit: Aldoni kiel kontrolanton nav: submit: Afiŝi - log_in: Ensaluti - log_out: Elsaluti + log_in: Saluti + log_out: Adiaŭi profile: Profilo register: Registri my_account: Mia konto user_settings: Uzanto-agordoj - forums: Forumoj + forums: Diskutejoj inbox_count: Enirkesto (%count%) messages: Mesaĝoj wiki: Vikio @@ -182,65 +182,65 @@ nav: previous: Antaŭa request_password_reset: - title: Peti pasvortan reŝargon ligilon + title: Peti ligilon al pasvorta restarigo multiple_accounts_notice: > - Se vi havas multaj kontoj estas registra kun unu retpoŝtadreso vi ricevos unu retpoŝto po konto. + Se vi havas multaj kontoj estas registritajn kun unu retpoŝtadreso, vi ricevos unu retleteron po konto. no_confirmation_notice: > - Se vi neniam ricevos retpoŝton, la adreso vi tajpis eble estas malprava. - Por privateca kialoj, ni ne konfirmos se uzanto kun retpoŝto ekzistas. + Se vi neniam ricevos retleteron, la adreso kiun vi donis eble estas malĝusta. + Por privatecaj kialoj, ni ne konfirmos ĉu uzanto kun retpoŝtadreso ekzistas. request_password_reset_form: email: Retpoŝtadreso submit: Sendi reset_password: - email_subject: '%site_name% - Reŝargi pasvorton por uzanto %username%' + email_subject: '%site_name% - Restarigi pasvorton por uzanto %username%' # this should be kept at 76 columns -------------------------------------------- email_body: | - Iu petis pasvortan reŝargon por via %site_name% konto. Por reŝargi pasvoton, - klicku la ligilo sub: + Iu petis pasvortan restarigon por via konto ĉe %site_name%. Por + restarigi pasvorton, alklaku la ĉi-suban ligilon: %reset_link% - Se vi ne petis pasvortan reŝargon, vi povas sendanĝere malatenti ĉi-tion - mesaĝon. + Se vi ne petis pasvortan restarigon, vi povas sendanĝere malatenti + ĉi tiun mesaĝon. - title: Reŝargi pasvorton + title: Restarigi pasvorton site_footer: - version: Uzanta %app% %version% (%branch%). Farita kun ☭. + version: Povigita de %app% %version% (%branch%). Farita kun ☭. submission_form: title: Titolo url: URL body: Korpo - forum: Forumo + forum: Diskutejo sticky: Gluita - create: Krei submission - edit: Rekonti submission - delete: Savi submission - confirm_delete: Ĉu vi certe volas savi ĉi-tion submission? + create: Krei afiŝon + edit: Redakti afiŝon + delete: Forigi afiŝon + confirm_delete: Ĉu vi certe volas forigi ĉi tiun afiŝon? submissions: comments: '{0} Sen komentoj|{1} %count% komento|[1,Inf[ %count% komentoj' edit: Redakti - info_with_forum_name: Afiŝis de %submitter% %timestamp% en %forum% - info_without_forum_name: Afiŝis de %submitter% %timestamp% + info_with_forum_name: Afiŝis %submitter% %timestamp% en %forum% + info_without_forum_name: Afiŝis %submitter% %timestamp% info_at_timestamp: je %timestamp% - sort_by_hot: Varma - sort_by_new: Nova - sort_by_top: Supra - sort_by_controversial: Polemika + sort_by_hot: Furoraj + sort_by_new: Novaj + sort_by_top: Supraj + sort_by_controversial: Disputigaj total_votes: '{1} %count% voĉdono|[0,Inf[ %count% voĉdonoj' vote_stats: (+%up%, −%down%) - ip_ban: IP forbaro + ip_ban: IP-forbaro edit_info: (redaktis %edited_at%) moderator_info: (kontrolanto redaktis %edit_at%) time: on_timestamp: je %timestamp% - earlier_format: '%relative_time% frue' + earlier_format: '%relative_time% antaŭe' later_format: '%relative_time% poste' title: @@ -250,14 +250,14 @@ user: submissions: Afiŝoj comments: Komentoj moderates: '%username% estas kontrolanto por:' - username_rules: Permesita karakteroj estas A ĝis Z, a ĝis z, 0 ĝis 9, kaj substrekas. - password_rules: Minimuma de 8 karakteroj; maksimuma de 72 ĉar limigoj de la «bcrypt» algoritmo. - email_optional: Provizi retpoŝton nedevigas. Ni nur uzos ĝin por reagordi pasvortojn kaj retpoŝti du-faktorajn identokontrolojn. + username_rules: Permesitaj signoj estas A ĝis Z, a ĝis z, 0 ĝis 9, kaj substrekoj. + password_rules: Minimume 8 signoj; maksimume 72 pro limigoj de la algoritmo «bcrypt». + email_optional: Retpoŝtadreso malnepras. Ni nur uzos ĝin por reagordi pasvortojn kaj retpoŝti du-fazajn identokontrolojn. message: Sendi mesaĝon - registered: Registris %timestamp% + registered: Registriĝis %timestamp% user_flag: - post_as_label: Afiŝi tiel + post_as_label: Afiŝi kiel none_label: (sen) admin: administranto admin_label: Administranto @@ -270,37 +270,37 @@ user_form: repeat_password: Pasvorto (ripetu) new_password: Nova pasvorto repeat_new_password: Nova pasvorto (ripetu) - email: Retpoŝto - register: Registri + email: Retpoŝtadreso + register: Registriĝi verification: Certigo - save: Konservi modifiĝojn + save: Konservi ŝanĝojn user_settings: - title: Redaktante uzanton-agordojn por %username% + title: Redaktante uzanto-agordojn por %username% user_settings_form: locale: Lingvo night_mode: Nokta reĝimo - save: Konservi modifiĝojn + save: Konservi ŝanĝojn wiki: not_found_title: Ne trovis paĝon. - not_found_message: La petitis paĝon ne trovitis. - create_this_page: Krei ĉi-tiun paĝon + not_found_message: Petita paĝo ne troviĝis. + create_this_page: Krei ĉi tiun paĝon editing_title: Redaktas "%path%" create_title: Kreas "%path%" history_title: Montras historion de "%path%" - revision_time: Redaktis + revision_time: Redaktita user: Uzanto - id_label: ID - locked_notice: La paĝon ŝlositis. Nur administrantoj povas redakti ĝin. + id_label: Identigilo + locked_notice: La paĝo estas ŝlosita. Nur administrantoj povas redakti ĝin. page_title: Paĝa Titolo path: Dosierindiko all_pages: Ĉiuj paĝoj page_history: Paĝa historio edit_page: Redakti paĝon navigation: Navigado - last_edited: Laste redaktis de %username% %timestamp% + last_edited: Laste redaktis %username% %timestamp% wiki_form: title: Titolo diff --git a/translations/security.eo.yml b/translations/security.eo.yml new file mode 100644 index 0000000..de46a26 --- /dev/null +++ b/translations/security.eo.yml @@ -0,0 +1 @@ +'Your account has been banned.': 'Via konto estas forbarita.' diff --git a/translations/validators.eo.yml b/translations/validators.eo.yml index cbb7e6a..0ece474 100644 --- a/translations/validators.eo.yml +++ b/translations/validators.eo.yml @@ -1,3 +1,3 @@ 'The comment must not be empty.': 'La komento devas ne malpleni.' -'The name must contain only contain letters, numbers, and underscores.': 'La nomo devas nur enhavi karakterojn, nombrojn, kaj substrekojn.' -'No such user.': 'Ne tia uzanto.' +'The name must contain only contain letters, numbers, and underscores.': 'La nomo devas enhavi nur signojn, nombrojn, kaj substrekojn.' +'No such user.': 'Neniu uzanto tia.' From 1c9a69a547fb0bd8727d3dd4c1621c36564039bc Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 5 Apr 2018 07:49:48 +0200 Subject: [PATCH 10/24] update dependencies (fixes cache warmup) --- composer.lock | 431 +++++++++++++++++++++++++------------------------- 1 file changed, 216 insertions(+), 215 deletions(-) diff --git a/composer.lock b/composer.lock index 16a5bb2..04e4803 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "943b2c4fcad1ef178d16a713c2468bf7e579c288" + "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/943b2c4fcad1ef178d16a713c2468bf7e579c288", - "reference": "943b2c4fcad1ef178d16a713c2468bf7e579c288", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169", + "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169", "shasum": "" }, "require": { @@ -26,7 +26,7 @@ "php": "^5.3.2 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", "psr/log": "^1.0", "symfony/process": "^2.5 || ^3.0 || ^4.0" }, @@ -60,7 +60,7 @@ "ssl", "tls" ], - "time": "2017-11-29T09:37:33+00:00" + "time": "2018-03-29T19:57:20+00:00" }, { "name": "doctrine/annotations", @@ -346,16 +346,16 @@ }, { "name": "doctrine/dbal", - "version": "v2.6.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13" + "reference": "f76bf5ef631cec551a86c2291fc749534febebf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/e3eed9b1facbb0ced3a0995244843a189e7d1b13", - "reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/f76bf5ef631cec551a86c2291fc749534febebf1", + "reference": "f76bf5ef631cec551a86c2291fc749534febebf1", "shasum": "" }, "require": { @@ -364,9 +364,11 @@ "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^5.4.6", + "doctrine/coding-standard": "^4.0", + "phpunit/phpunit": "^7.0", "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5", - "symfony/console": "2.*||^3.0" + "symfony/console": "^2.0.5||^3.0", + "symfony/phpunit-bridge": "^3.4.5|^4.0.5" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -377,7 +379,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev" + "dev-master": "2.7.x-dev" } }, "autoload": { @@ -415,7 +417,7 @@ "persistence", "queryobject" ], - "time": "2017-11-19T13:38:54+00:00" + "time": "2018-04-01T23:33:17+00:00" }, { "name": "doctrine/doctrine-bundle", @@ -504,39 +506,39 @@ }, { "name": "doctrine/doctrine-cache-bundle", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineCacheBundle.git", - "reference": "9baecbd6bfdd1123b0cf8c1b88fee0170a84ddd1" + "reference": "4c8e363f96427924e7e519c5b5119b4f54512697" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineCacheBundle/zipball/9baecbd6bfdd1123b0cf8c1b88fee0170a84ddd1", - "reference": "9baecbd6bfdd1123b0cf8c1b88fee0170a84ddd1", + "url": "https://api.github.com/repos/doctrine/DoctrineCacheBundle/zipball/4c8e363f96427924e7e519c5b5119b4f54512697", + "reference": "4c8e363f96427924e7e519c5b5119b4f54512697", "shasum": "" }, "require": { "doctrine/cache": "^1.4.2", "doctrine/inflector": "~1.0", "php": ">=5.3.2", - "symfony/doctrine-bridge": "~2.2|~3.0|~4.0" + "symfony/doctrine-bridge": "~2.7|~3.3|~4.0" }, "require-dev": { "instaclick/coding-standard": "~1.1", "instaclick/object-calisthenics-sniffs": "dev-master", "instaclick/symfony2-coding-standard": "dev-remaster", - "phpunit/phpunit": "~4", + "phpunit/phpunit": "~4|~5", "predis/predis": "~0.8", "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "~1.5", - "symfony/console": "~2.2|~3.0|~4.0", - "symfony/finder": "~2.2|~3.0|~4.0", - "symfony/framework-bundle": "~2.2|~3.0|~4.0", - "symfony/phpunit-bridge": "~2.7|~3.0|~4.0", - "symfony/security-acl": "~2.3|~3.0", - "symfony/validator": "~2.2|~3.0|~4.0", - "symfony/yaml": "~2.2|~3.0|~4.0" + "symfony/console": "~2.7|~3.3|~4.0", + "symfony/finder": "~2.7|~3.3|~4.0", + "symfony/framework-bundle": "~2.7|~3.3|~4.0", + "symfony/phpunit-bridge": "~2.7|~3.3|~4.0", + "symfony/security-acl": "~2.7|~3.3", + "symfony/validator": "~2.7|~3.3|~4.0", + "symfony/yaml": "~2.7|~3.3|~4.0" }, "suggest": { "symfony/security-acl": "For using this bundle to cache ACLs" @@ -588,7 +590,7 @@ "cache", "caching" ], - "time": "2017-10-12T17:23:29+00:00" + "time": "2018-03-27T09:22:12+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1420,16 +1422,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + "reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/68d0ea14d5a3f42a20e87632a5f84931e2709c90", + "reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90", "shasum": "" }, "require": { @@ -1439,7 +1441,7 @@ }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4", "psr/log": "^1.0" }, "suggest": { @@ -1448,7 +1450,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.2-dev" + "dev-master": "6.3-dev" } }, "autoload": { @@ -1481,7 +1483,7 @@ "rest", "web service" ], - "time": "2017-06-22T18:50:49+00:00" + "time": "2018-03-26T16:33:04+00:00" }, { "name": "guzzlehttp/promises", @@ -1709,16 +1711,16 @@ }, { "name": "league/commonmark", - "version": "0.17.1", + "version": "0.17.5", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "38b2fca6b8493abd9314fcf6b5b042cfba6a0f11" + "reference": "82d7ab62d7f68391cb9d323f3ccce50be24a5369" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/38b2fca6b8493abd9314fcf6b5b042cfba6a0f11", - "reference": "38b2fca6b8493abd9314fcf6b5b042cfba6a0f11", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/82d7ab62d7f68391cb9d323f3ccce50be24a5369", + "reference": "82d7ab62d7f68391cb9d323f3ccce50be24a5369", "shasum": "" }, "require": { @@ -1774,7 +1776,7 @@ "markdown", "parser" ], - "time": "2018-03-18T13:01:30+00:00" + "time": "2018-03-29T14:35:19+00:00" }, { "name": "league/flysystem", @@ -2320,16 +2322,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.11", + "version": "v2.0.12", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" + "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb", + "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb", "shasum": "" }, "require": { @@ -2364,7 +2366,7 @@ "pseudorandom", "random" ], - "time": "2017-09-27T21:40:39+00:00" + "time": "2018-04-04T21:24:14+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3169,7 +3171,7 @@ }, { "name": "symfony/asset", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", @@ -3225,16 +3227,16 @@ }, { "name": "symfony/cache", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "fcffcf7f26d232b64329f37182defe253caa06b0" + "reference": "681c245e629409a2f1ded6bf783e833d291d8af2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/fcffcf7f26d232b64329f37182defe253caa06b0", - "reference": "fcffcf7f26d232b64329f37182defe253caa06b0", + "url": "https://api.github.com/repos/symfony/cache/zipball/681c245e629409a2f1ded6bf783e833d291d8af2", + "reference": "681c245e629409a2f1ded6bf783e833d291d8af2", "shasum": "" }, "require": { @@ -3290,20 +3292,20 @@ "caching", "psr6" ], - "time": "2018-02-11T17:17:44+00:00" + "time": "2018-04-02T14:35:51+00:00" }, { "name": "symfony/config", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "289eadd3771f7682ea2540e4925861c18ec5b4d0" + "reference": "7c19370ab04e9ac05b74a504198e165f5ccf6dd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/289eadd3771f7682ea2540e4925861c18ec5b4d0", - "reference": "289eadd3771f7682ea2540e4925861c18ec5b4d0", + "url": "https://api.github.com/repos/symfony/config/zipball/7c19370ab04e9ac05b74a504198e165f5ccf6dd8", + "reference": "7c19370ab04e9ac05b74a504198e165f5ccf6dd8", "shasum": "" }, "require": { @@ -3352,20 +3354,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-02-04T16:43:51+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/console", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "555c8dbe0ae9e561740451eabdbed2cc554b6a51" + "reference": "aad9a6fe47319f22748fd764f52d3a7ca6fa6b64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/555c8dbe0ae9e561740451eabdbed2cc554b6a51", - "reference": "555c8dbe0ae9e561740451eabdbed2cc554b6a51", + "url": "https://api.github.com/repos/symfony/console/zipball/aad9a6fe47319f22748fd764f52d3a7ca6fa6b64", + "reference": "aad9a6fe47319f22748fd764f52d3a7ca6fa6b64", "shasum": "" }, "require": { @@ -3420,20 +3422,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-02-26T15:55:47+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "symfony/debug", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "1721e4e7effb23480966690cdcdc7d2a4152d489" + "reference": "5961d02d48828671f5d8a7805e06579d692f6ede" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1721e4e7effb23480966690cdcdc7d2a4152d489", - "reference": "1721e4e7effb23480966690cdcdc7d2a4152d489", + "url": "https://api.github.com/repos/symfony/debug/zipball/5961d02d48828671f5d8a7805e06579d692f6ede", + "reference": "5961d02d48828671f5d8a7805e06579d692f6ede", "shasum": "" }, "require": { @@ -3476,20 +3478,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-02-28T21:50:02+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "93ad14f124beacf16894b64bb5b3cdd5b4367e38" + "reference": "9f1cea656afc5512c6f5e58d61fcea12acee113e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/93ad14f124beacf16894b64bb5b3cdd5b4367e38", - "reference": "93ad14f124beacf16894b64bb5b3cdd5b4367e38", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9f1cea656afc5512c6f5e58d61fcea12acee113e", + "reference": "9f1cea656afc5512c6f5e58d61fcea12acee113e", "shasum": "" }, "require": { @@ -3547,20 +3549,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-03-05T18:28:26+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "404becb75de28db14134d662f5b9b78b9a262acb" + "reference": "6743ff309bee333f885b237d04e81652f73f51a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/404becb75de28db14134d662f5b9b78b9a262acb", - "reference": "404becb75de28db14134d662f5b9b78b9a262acb", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/6743ff309bee333f885b237d04e81652f73f51a3", + "reference": "6743ff309bee333f885b237d04e81652f73f51a3", "shasum": "" }, "require": { @@ -3626,11 +3628,11 @@ ], "description": "Symfony Doctrine Bridge", "homepage": "https://symfony.com", - "time": "2018-02-04T13:08:26+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3693,7 +3695,7 @@ }, { "name": "symfony/expression-language", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", @@ -3743,7 +3745,7 @@ }, { "name": "symfony/filesystem", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3792,16 +3794,16 @@ }, { "name": "symfony/finder", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "44a796d2ecc2a16a5fc8f2956a34ee617934d55f" + "reference": "c72995d9f5999b3fcdd8660c0c9690243252e1e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/44a796d2ecc2a16a5fc8f2956a34ee617934d55f", - "reference": "44a796d2ecc2a16a5fc8f2956a34ee617934d55f", + "url": "https://api.github.com/repos/symfony/finder/zipball/c72995d9f5999b3fcdd8660c0c9690243252e1e1", + "reference": "c72995d9f5999b3fcdd8660c0c9690243252e1e1", "shasum": "" }, "require": { @@ -3837,20 +3839,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-03-05T18:28:26+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/flex", - "version": "v1.0.76", + "version": "v1.0.78", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "202c981673b255a41bac4076cb82629a0b8481cb" + "reference": "eead30b31db70691cd1fd1e7225190c818a1a5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/202c981673b255a41bac4076cb82629a0b8481cb", - "reference": "202c981673b255a41bac4076cb82629a0b8481cb", + "url": "https://api.github.com/repos/symfony/flex/zipball/eead30b31db70691cd1fd1e7225190c818a1a5f6", + "reference": "eead30b31db70691cd1fd1e7225190c818a1a5f6", "shasum": "" }, "require": { @@ -3883,20 +3885,20 @@ "email": "fabien.potencier@gmail.com" } ], - "time": "2018-03-23T16:30:50+00:00" + "time": "2018-03-27T10:04:58+00:00" }, { "name": "symfony/form", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "26c2749671eb73602b4264e4a3221fb451dbc8d5" + "reference": "5a3f505b256a65978d1d4f924ed53ceb503d1d5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/26c2749671eb73602b4264e4a3221fb451dbc8d5", - "reference": "26c2749671eb73602b4264e4a3221fb451dbc8d5", + "url": "https://api.github.com/repos/symfony/form/zipball/5a3f505b256a65978d1d4f924ed53ceb503d1d5f", + "reference": "5a3f505b256a65978d1d4f924ed53ceb503d1d5f", "shasum": "" }, "require": { @@ -3963,20 +3965,20 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2018-03-01T10:21:51+00:00" + "time": "2018-04-02T10:05:09+00:00" }, { "name": "symfony/framework-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "d47d6da8c852648e26f12e55e4c895b81c4e99bf" + "reference": "a8e73bb539054efdca59a4ec0d3af99635812505" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d47d6da8c852648e26f12e55e4c895b81c4e99bf", - "reference": "d47d6da8c852648e26f12e55e4c895b81c4e99bf", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a8e73bb539054efdca59a4ec0d3af99635812505", + "reference": "a8e73bb539054efdca59a4ec0d3af99635812505", "shasum": "" }, "require": { @@ -4077,20 +4079,20 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2018-03-02T08:28:17+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6c181e81a3a9a7996c62ebd7803592536e729c5a" + "reference": "d0864a82e5891ab61d31eecbaa48bed5a09b8e6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6c181e81a3a9a7996c62ebd7803592536e729c5a", - "reference": "6c181e81a3a9a7996c62ebd7803592536e729c5a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0864a82e5891ab61d31eecbaa48bed5a09b8e6c", + "reference": "d0864a82e5891ab61d31eecbaa48bed5a09b8e6c", "shasum": "" }, "require": { @@ -4130,20 +4132,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-03-05T16:01:10+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "2a1ebfe8c37240500befcb17bceb3893adacffa3" + "reference": "38337d03a554a2b0e9f553d368723692b7c04a8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2a1ebfe8c37240500befcb17bceb3893adacffa3", - "reference": "2a1ebfe8c37240500befcb17bceb3893adacffa3", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/38337d03a554a2b0e9f553d368723692b7c04a8f", + "reference": "38337d03a554a2b0e9f553d368723692b7c04a8f", "shasum": "" }, "require": { @@ -4216,11 +4218,11 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-03-05T22:27:01+00:00" + "time": "2018-04-03T06:20:33+00:00" }, { "name": "symfony/inflector", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/inflector.git", @@ -4277,16 +4279,16 @@ }, { "name": "symfony/intl", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "929953d9e64c7209afb907fc34ac64b135c79cb8" + "reference": "d58df88e3cfdb2702f5fd8cd67c33961c2539e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/929953d9e64c7209afb907fc34ac64b135c79cb8", - "reference": "929953d9e64c7209afb907fc34ac64b135c79cb8", + "url": "https://api.github.com/repos/symfony/intl/zipball/d58df88e3cfdb2702f5fd8cd67c33961c2539e0c", + "reference": "d58df88e3cfdb2702f5fd8cd67c33961c2539e0c", "shasum": "" }, "require": { @@ -4348,11 +4350,11 @@ "l10n", "localization" ], - "time": "2018-02-03T00:57:23+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", @@ -4481,7 +4483,7 @@ }, { "name": "symfony/options-resolver", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -4740,16 +4742,16 @@ }, { "name": "symfony/process", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6ed08502a7c9559da8e60ea343bdbd19c3350b3e" + "reference": "d7dc1ee5dfe9f732cb1bba7310f5b99f2b7a6d25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6ed08502a7c9559da8e60ea343bdbd19c3350b3e", - "reference": "6ed08502a7c9559da8e60ea343bdbd19c3350b3e", + "url": "https://api.github.com/repos/symfony/process/zipball/d7dc1ee5dfe9f732cb1bba7310f5b99f2b7a6d25", + "reference": "d7dc1ee5dfe9f732cb1bba7310f5b99f2b7a6d25", "shasum": "" }, "require": { @@ -4785,11 +4787,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-02-19T12:18:43+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "symfony/property-access", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", @@ -4856,7 +4858,7 @@ }, { "name": "symfony/property-info", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", @@ -4980,16 +4982,16 @@ }, { "name": "symfony/routing", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "9c6268c1970c7e507bedc8946bece32a7db23515" + "reference": "ca780c838046bfef4a6fd50284ae71a5d1f1a8b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/9c6268c1970c7e507bedc8946bece32a7db23515", - "reference": "9c6268c1970c7e507bedc8946bece32a7db23515", + "url": "https://api.github.com/repos/symfony/routing/zipball/ca780c838046bfef4a6fd50284ae71a5d1f1a8b2", + "reference": "ca780c838046bfef4a6fd50284ae71a5d1f1a8b2", "shasum": "" }, "require": { @@ -5054,20 +5056,20 @@ "uri", "url" ], - "time": "2018-02-28T21:50:02+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/security", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/security.git", - "reference": "a0a161c5b28a4436d955dd6385c2ebbeca1852c3" + "reference": "3e08200cc978c71d2cfdbc9500db4f2b65bdd28f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security/zipball/a0a161c5b28a4436d955dd6385c2ebbeca1852c3", - "reference": "a0a161c5b28a4436d955dd6385c2ebbeca1852c3", + "url": "https://api.github.com/repos/symfony/security/zipball/3e08200cc978c71d2cfdbc9500db4f2b65bdd28f", + "reference": "3e08200cc978c71d2cfdbc9500db4f2b65bdd28f", "shasum": "" }, "require": { @@ -5131,20 +5133,20 @@ ], "description": "Symfony Security Component", "homepage": "https://symfony.com", - "time": "2018-02-23T14:40:28+00:00" + "time": "2018-04-02T14:35:51+00:00" }, { "name": "symfony/security-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "5cbcf5d33261aaa0b5895a253161c6ba32754efd" + "reference": "4edcb55716cdbbb26783313f34046ada778c87c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/5cbcf5d33261aaa0b5895a253161c6ba32754efd", - "reference": "5cbcf5d33261aaa0b5895a253161c6ba32754efd", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/4edcb55716cdbbb26783313f34046ada778c87c5", + "reference": "4edcb55716cdbbb26783313f34046ada778c87c5", "shasum": "" }, "require": { @@ -5211,20 +5213,20 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "time": "2018-02-19T12:18:43+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/serializer", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "0526b430c8548520fd5817f425d9a3d8a11a21e1" + "reference": "d0e9269101ccd978af971fd4d7a892848672bfa1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/0526b430c8548520fd5817f425d9a3d8a11a21e1", - "reference": "0526b430c8548520fd5817f425d9a3d8a11a21e1", + "url": "https://api.github.com/repos/symfony/serializer/zipball/d0e9269101ccd978af971fd4d7a892848672bfa1", + "reference": "d0e9269101ccd978af971fd4d7a892848672bfa1", "shasum": "" }, "require": { @@ -5289,7 +5291,7 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2018-02-19T12:18:43+00:00" + "time": "2018-03-19T17:30:36+00:00" }, { "name": "symfony/serializer-pack", @@ -5324,16 +5326,16 @@ }, { "name": "symfony/swiftmailer-bundle", - "version": "v3.2.1", + "version": "v3.2.2", "source": { "type": "git", "url": "https://github.com/symfony/swiftmailer-bundle.git", - "reference": "20e71c247a5a43ceb655db9712394d08c09b33ef" + "reference": "f1ba0552a9cd4df0191a58845fbd5541cf9eda2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/20e71c247a5a43ceb655db9712394d08c09b33ef", - "reference": "20e71c247a5a43ceb655db9712394d08c09b33ef", + "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/f1ba0552a9cd4df0191a58845fbd5541cf9eda2d", + "reference": "f1ba0552a9cd4df0191a58845fbd5541cf9eda2d", "shasum": "" }, "require": { @@ -5382,11 +5384,11 @@ ], "description": "Symfony SwiftmailerBundle", "homepage": "http://symfony.com", - "time": "2018-03-08T16:39:26+00:00" + "time": "2018-04-03T16:29:41+00:00" }, { "name": "symfony/templating", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/templating.git", @@ -5441,7 +5443,7 @@ }, { "name": "symfony/translation", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -5509,16 +5511,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "575004ae3bcfb7d909a34db20edb7c349defb092" + "reference": "7596e74f91d9c2ecb5de35811b87655e9533096f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/575004ae3bcfb7d909a34db20edb7c349defb092", - "reference": "575004ae3bcfb7d909a34db20edb7c349defb092", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/7596e74f91d9c2ecb5de35811b87655e9533096f", + "reference": "7596e74f91d9c2ecb5de35811b87655e9533096f", "shasum": "" }, "require": { @@ -5527,7 +5529,7 @@ }, "conflict": { "symfony/console": "<3.4", - "symfony/form": "<3.4.5|<4.0.5,>=4.0" + "symfony/form": "<3.4.7|<4.0.7,>=4.0" }, "require-dev": { "symfony/asset": "~3.4|~4.0", @@ -5535,7 +5537,7 @@ "symfony/dependency-injection": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", - "symfony/form": "^3.4.5|^4.0.5", + "symfony/form": "^3.4.7|^4.0.7", "symfony/http-foundation": "~3.4|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/polyfill-intl-icu": "~1.0", @@ -5595,11 +5597,11 @@ ], "description": "Symfony Twig Bridge", "homepage": "https://symfony.com", - "time": "2018-03-01T10:21:51+00:00" + "time": "2018-04-02T14:06:14+00:00" }, { "name": "symfony/twig-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", @@ -5672,16 +5674,16 @@ }, { "name": "symfony/validator", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "56f72db4783d715a6742a783813362b514a77c1d" + "reference": "2f080fe8af133d12bdf3eeb55a9d01e1286f8e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/56f72db4783d715a6742a783813362b514a77c1d", - "reference": "56f72db4783d715a6742a783813362b514a77c1d", + "url": "https://api.github.com/repos/symfony/validator/zipball/2f080fe8af133d12bdf3eeb55a9d01e1286f8e24", + "reference": "2f080fe8af133d12bdf3eeb55a9d01e1286f8e24", "shasum": "" }, "require": { @@ -5752,11 +5754,11 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-03-01T10:21:51+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/web-link", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", @@ -5827,16 +5829,16 @@ }, { "name": "symfony/yaml", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "de5f125ea39de846b90b313b2cfb031a0152d223" + "reference": "8b34ebb5989df61cbd77eff29a02c4db9ac1069c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/de5f125ea39de846b90b313b2cfb031a0152d223", - "reference": "de5f125ea39de846b90b313b2cfb031a0152d223", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8b34ebb5989df61cbd77eff29a02c4db9ac1069c", + "reference": "8b34ebb5989df61cbd77eff29a02c4db9ac1069c", "shasum": "" }, "require": { @@ -5881,7 +5883,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-02-19T20:08:53+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "twig/extensions", @@ -5941,16 +5943,16 @@ }, { "name": "twig/twig", - "version": "v2.4.7", + "version": "v2.4.8", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc" + "reference": "7b604c89da162034bdf4bb66310f358d313dd16d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/69aacd44dbbaa3199d5afb68605c996d577896fc", - "reference": "69aacd44dbbaa3199d5afb68605c996d577896fc", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7b604c89da162034bdf4bb66310f358d313dd16d", + "reference": "7b604c89da162034bdf4bb66310f358d313dd16d", "shasum": "" }, "require": { @@ -6003,7 +6005,7 @@ "keywords": [ "templating" ], - "time": "2018-03-20T04:31:17+00:00" + "time": "2018-04-02T09:24:19+00:00" }, { "name": "webmozart/assert", @@ -6810,16 +6812,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "fee0fcd501304b1c3190f6293f650cceb738a353" + "reference": "c43bfa0182363b3fd64331b5e64e467349ff4670" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/fee0fcd501304b1c3190f6293f650cceb738a353", - "reference": "fee0fcd501304b1c3190f6293f650cceb738a353", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c43bfa0182363b3fd64331b5e64e467349ff4670", + "reference": "c43bfa0182363b3fd64331b5e64e467349ff4670", "shasum": "" }, "require": { @@ -6863,20 +6865,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:38:00+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/css-selector", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "c69f1e93aa898fd9fec627ebef467188151c8dc2" + "reference": "03f965583147957f1ecbad7ea1c9d6fd5e525ec2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/c69f1e93aa898fd9fec627ebef467188151c8dc2", - "reference": "c69f1e93aa898fd9fec627ebef467188151c8dc2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/03f965583147957f1ecbad7ea1c9d6fd5e525ec2", + "reference": "03f965583147957f1ecbad7ea1c9d6fd5e525ec2", "shasum": "" }, "require": { @@ -6916,11 +6918,11 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-02-03T14:58:37+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/debug-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", @@ -6985,16 +6987,16 @@ }, { "name": "symfony/debug-pack", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/symfony/debug-pack.git", - "reference": "2da9b0ea7a733fc29ec3e72139751fd8d2582887" + "reference": "ae4b15596788f9592f67f8615398671bf3f796e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-pack/zipball/2da9b0ea7a733fc29ec3e72139751fd8d2582887", - "reference": "2da9b0ea7a733fc29ec3e72139751fd8d2582887", + "url": "https://api.github.com/repos/symfony/debug-pack/zipball/ae4b15596788f9592f67f8615398671bf3f796e4", + "reference": "ae4b15596788f9592f67f8615398671bf3f796e4", "shasum": "" }, "require": { @@ -7002,7 +7004,6 @@ "php": "^7.0", "symfony/debug-bundle": "^3.3|^4.0", "symfony/monolog-bundle": "^3.0", - "symfony/phpunit-bridge": "^3.3|^4.0", "symfony/profiler-pack": "^1.0", "symfony/var-dumper": "^3.3|^4.0" }, @@ -7012,20 +7013,20 @@ "MIT" ], "description": "A debug pack for Symfony projects", - "time": "2017-12-12T01:47:04+00:00" + "time": "2018-03-29T13:53:44+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "26726ddc01601dc9393f2afc3369ce1ca64e4537" + "reference": "d6c04c7532535b5e0b63db45b543cd60818e0fbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/26726ddc01601dc9393f2afc3369ce1ca64e4537", - "reference": "26726ddc01601dc9393f2afc3369ce1ca64e4537", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d6c04c7532535b5e0b63db45b543cd60818e0fbc", + "reference": "d6c04c7532535b5e0b63db45b543cd60818e0fbc", "shasum": "" }, "require": { @@ -7068,11 +7069,11 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-02-22T10:50:29+00:00" + "time": "2018-03-19T22:35:49+00:00" }, { "name": "symfony/dotenv", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", @@ -7129,16 +7130,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "14ffbbe2a72d0f6339b24eb830dd38cf63ba6630" + "reference": "cdfbf48b17ca56bb7fbcb4126f949665e5f426a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/14ffbbe2a72d0f6339b24eb830dd38cf63ba6630", - "reference": "14ffbbe2a72d0f6339b24eb830dd38cf63ba6630", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/cdfbf48b17ca56bb7fbcb4126f949665e5f426a3", + "reference": "cdfbf48b17ca56bb7fbcb4126f949665e5f426a3", "shasum": "" }, "require": { @@ -7191,7 +7192,7 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", - "time": "2018-02-19T16:50:22+00:00" + "time": "2018-03-22T13:42:36+00:00" }, { "name": "symfony/polyfill-php72", @@ -7278,7 +7279,7 @@ }, { "name": "symfony/stopwatch", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -7327,16 +7328,16 @@ }, { "name": "symfony/var-dumper", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c7d89044ed6ed3b7d8b558d509cca0666b947e58" + "reference": "f9b257f2009c7f09166bcfd3d3d96455741ed371" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c7d89044ed6ed3b7d8b558d509cca0666b947e58", - "reference": "c7d89044ed6ed3b7d8b558d509cca0666b947e58", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f9b257f2009c7f09166bcfd3d3d96455741ed371", + "reference": "f9b257f2009c7f09166bcfd3d3d96455741ed371", "shasum": "" }, "require": { @@ -7392,20 +7393,20 @@ "debug", "dump" ], - "time": "2018-02-26T15:55:47+00:00" + "time": "2018-04-03T05:24:00+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de" + "reference": "4d709edab20fdfc889a5c9a2b530453e4a4b8127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de", - "reference": "eaaf46b1ccaf83c8c97efd8d081e03d9ff0f29de", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4d709edab20fdfc889a5c9a2b530453e4a4b8127", + "reference": "4d709edab20fdfc889a5c9a2b530453e4a4b8127", "shasum": "" }, "require": { @@ -7458,11 +7459,11 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2018-03-02T08:28:17+00:00" + "time": "2018-04-02T09:52:41+00:00" }, { "name": "symfony/web-server-bundle", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/symfony/web-server-bundle.git", From 59570284c31a951ec200fd2a1f3bc58f0500d3bf Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 5 Apr 2018 08:04:44 +0200 Subject: [PATCH 11/24] replace deprecated psr-0 type hints with psr-4 --- tests/Security/UserCheckerTest.php | 5 +++-- tests/Security/Voter/TokenVoterTest.php | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Security/UserCheckerTest.php b/tests/Security/UserCheckerTest.php index 24f2794..86bca5e 100644 --- a/tests/Security/UserCheckerTest.php +++ b/tests/Security/UserCheckerTest.php @@ -4,6 +4,7 @@ use App\Entity\User; use App\Security\UserChecker; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -14,7 +15,7 @@ class UserCheckerTest extends TestCase { * @doesNotPerformAssertions */ public function testNonBannedUserDoesNotCauseExceptionOnAuth() { - /** @var User|\PHPUnit_Framework_MockObject_MockObject $user */ + /* @var User|MockObject $user */ $user = $this->createMock(User::class); $user->method('isBanned')->willReturn(false, false); @@ -25,7 +26,7 @@ public function testNonBannedUserDoesNotCauseExceptionOnAuth() { * @expectedException \App\Security\Exception\AccountBannedException */ public function testBannedUserCausesExceptionOnPostAuth() { - /** @var User|\PHPUnit_Framework_MockObject_MockObject $user */ + /* @var User|MockObject $user */ $user = $this->createMock(User::class); $user->method('isBanned')->willReturn(true); diff --git a/tests/Security/Voter/TokenVoterTest.php b/tests/Security/Voter/TokenVoterTest.php index 4366ac6..b3f54a1 100644 --- a/tests/Security/Voter/TokenVoterTest.php +++ b/tests/Security/Voter/TokenVoterTest.php @@ -4,6 +4,7 @@ use App\Entity\User; use App\Security\Voter\TokenVoter; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -57,7 +58,7 @@ private function getToken($roles, $createdAt) { $user->method('getCreated') ->willReturn((new \DateTime('@'.time()))->modify($createdAt)); - /* @var TokenInterface|\PHPUnit_Framework_MockObject_MockObject $token */ + /* @var TokenInterface|MockObject $token */ $token = $this->createMock(TokenInterface::class); $token->method('getRoles')->willReturn($roles); From b03c09530b70f4c3c4e87cd159673774aff4b141 Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 7 Apr 2018 11:00:09 +0200 Subject: [PATCH 12/24] add NotForumBanned constraint/validator --- src/Controller/CommentController.php | 6 +- src/Form/Model/CommentData.php | 49 ++++++------ src/Form/Model/SubmissionData.php | 2 + src/Validator/Constraints/NotForumBanned.php | 24 ++++++ .../Constraints/NotForumBannedValidator.php | 78 +++++++++++++++++++ translations/validators.en.yml | 1 + 6 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 src/Validator/Constraints/NotForumBanned.php create mode 100644 src/Validator/Constraints/NotForumBannedValidator.php diff --git a/src/Controller/CommentController.php b/src/Controller/CommentController.php index f728460..f1acd27 100644 --- a/src/Controller/CommentController.php +++ b/src/Controller/CommentController.php @@ -93,15 +93,13 @@ public function comment( EventDispatcherInterface $dispatcher, Comment $comment = null ) { - $data = new CommentData(); + $data = new CommentData($submission); $form = $this->createForm(CommentType::class, $data, ['forum' => $forum]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $user = $this->getUser(); - $ip = $request->getClientIp(); - $reply = $data->toComment($submission, $user, $comment, $ip); + $reply = $data->toComment($this->getUser(), $comment, $request->getClientIp()); $em->persist($reply); $em->flush(); diff --git a/src/Form/Model/CommentData.php b/src/Form/Model/CommentData.php index 3033a92..7669467 100644 --- a/src/Form/Model/CommentData.php +++ b/src/Form/Model/CommentData.php @@ -6,9 +6,18 @@ use App\Entity\Submission; use App\Entity\User; use App\Entity\UserFlags; +use App\Validator\Constraints\NotForumBanned; use Symfony\Component\Validator\Constraints as Assert; +/** + * @NotForumBanned(forumPath="submission.forum", errorPath="body") + */ class CommentData { + /** + * @var Submission + */ + private $submission; + /** * @Assert\NotBlank(message="The comment must not be empty.") * @Assert\Regex("/[[:graph:]]/u", message="The comment must not be empty.") @@ -24,23 +33,23 @@ class CommentData { private $userFlag = UserFlags::FLAG_NONE; public static function createFromComment(Comment $comment): self { - $self = new self(); + $self = new self($comment->getSubmission()); + $self->submission = $comment->getSubmission(); $self->body = $comment->getBody(); $self->userFlag = $comment->getUserFlag(); return $self; } - public function toComment( - Submission $submission, - User $user, - Comment $parent = null, - $ip = null - ): Comment { + public function __construct(Submission $submission) { + $this->submission = $submission; + } + + public function toComment(User $user, Comment $parent = null, $ip = null): Comment { return new Comment( $this->body, $user, - $submission, + $this->submission, $this->userFlag, $parent, $ip @@ -60,31 +69,23 @@ public function updateComment(Comment $comment, User $editingUser) { } } - /** - * @return string|null - */ - public function getBody() { + public function getBody(): ?string { return $this->body; } - /** - * @param string|null $body - */ - public function setBody($body) { + public function setBody($body): void { $this->body = $body; } - /** - * @return int|null - */ - public function getUserFlag() { + public function getUserFlag(): ?int { return $this->userFlag; } - /** - * @param int|null $userFlag - */ - public function setUserFlag($userFlag) { + public function setUserFlag($userFlag): void { $this->userFlag = $userFlag; } + + public function getSubmission(): Submission { + return $this->submission; + } } diff --git a/src/Form/Model/SubmissionData.php b/src/Form/Model/SubmissionData.php index a7862c5..cda1fff 100644 --- a/src/Form/Model/SubmissionData.php +++ b/src/Form/Model/SubmissionData.php @@ -6,6 +6,7 @@ use App\Entity\Submission; use App\Entity\User; use App\Entity\UserFlags; +use App\Validator\Constraints\NotForumBanned; use App\Validator\Constraints\RateLimit; use Symfony\Component\Validator\Constraints as Assert; @@ -44,6 +45,7 @@ class SubmissionData { private $userFlag = UserFlags::FLAG_NONE; /** + * @NotForumBanned() * @Assert\NotBlank(groups={"create", "edit"}) * * @var Forum|null diff --git a/src/Validator/Constraints/NotForumBanned.php b/src/Validator/Constraints/NotForumBanned.php new file mode 100644 index 0000000..cb10906 --- /dev/null +++ b/src/Validator/Constraints/NotForumBanned.php @@ -0,0 +1,24 @@ + 'FORUM_BANNED_ERROR', + ]; + + public function getTargets(): array { + return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; + } +} diff --git a/src/Validator/Constraints/NotForumBannedValidator.php b/src/Validator/Constraints/NotForumBannedValidator.php new file mode 100644 index 0000000..ba9143f --- /dev/null +++ b/src/Validator/Constraints/NotForumBannedValidator.php @@ -0,0 +1,78 @@ +tokenStorage = $tokenStorage; + } + + public function validate($value, Constraint $constraint): void { + if (!$value || !$this->tokenStorage->getToken() || !$this->tokenStorage->getToken()->getUser()) { + return; + } + + if (!\is_object($value)) { + throw new UnexpectedTypeException($value, 'object'); + } + + if (!$constraint instanceof NotForumBanned) { + throw new UnexpectedTypeException($constraint, NotForumBanned::class); + } + + if ($constraint->forumPath) { + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + + if (!$propertyAccessor->isReadable($value, $constraint->forumPath)) { + throw new InvalidArgumentException(sprintf( + 'Cannot read property %s on object of type %s', + $constraint->forumPath, + \get_class($value) + )); + } + + $forum = $propertyAccessor->getValue($value, $constraint->forumPath); + } else { + $forum = $value; + } + + if ($forum === null) { + return; + } + + if (!$forum instanceof Forum) { + throw new InvalidArgumentException(sprintf( + 'Property %s on object of type %s is not of type %s', + $constraint->forumPath, + \get_class($value), + Forum::class + )); + } + + $token = $this->tokenStorage->getToken(); + + if (!$token) { + return; + } + + if ($forum->userIsBanned($token->getUser())) { + $this->context->buildViolation($constraint->message) + ->setCode(NotForumBanned::FORUM_BANNED_ERROR) + ->atPath($constraint->errorPath) + ->addViolation(); + } + } +} diff --git a/translations/validators.en.yml b/translations/validators.en.yml index a0673fb..91f6fbe 100644 --- a/translations/validators.en.yml +++ b/translations/validators.en.yml @@ -20,3 +20,4 @@ 'The IP address is not valid.': 'The IP address is not valid.' 'The CIDR mask is not valid.': 'The CIDR mask is not valid.' 'Missing CIDR mask.': 'Missing CIDR mask.' +'You have been banned from this forum.': 'You have been banned from this forum.' From 097ee454c30cf826b5150d325f9f9c5897128cff Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 7 Apr 2018 11:09:26 +0200 Subject: [PATCH 13/24] update dependencies --- composer.lock | 192 +++++++++++++++++++++++++------------------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/composer.lock b/composer.lock index 04e4803..fbd153c 100644 --- a/composer.lock +++ b/composer.lock @@ -1780,16 +1780,16 @@ }, { "name": "league/flysystem", - "version": "1.0.43", + "version": "1.0.44", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "1ce7cc142d906ba58dc54c82915d355a9191c8a8" + "reference": "168dbe519737221dc87d17385cde33073881fd02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1ce7cc142d906ba58dc54c82915d355a9191c8a8", - "reference": "1ce7cc142d906ba58dc54c82915d355a9191c8a8", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/168dbe519737221dc87d17385cde33073881fd02", + "reference": "168dbe519737221dc87d17385cde33073881fd02", "shasum": "" }, "require": { @@ -1860,7 +1860,7 @@ "sftp", "storage" ], - "time": "2018-03-01T10:27:04+00:00" + "time": "2018-04-06T09:58:14+00:00" }, { "name": "liip/imagine-bundle", @@ -3171,7 +3171,7 @@ }, { "name": "symfony/asset", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", @@ -3227,7 +3227,7 @@ }, { "name": "symfony/cache", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", @@ -3296,7 +3296,7 @@ }, { "name": "symfony/config", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -3358,7 +3358,7 @@ }, { "name": "symfony/console", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -3426,7 +3426,7 @@ }, { "name": "symfony/debug", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -3482,7 +3482,7 @@ }, { "name": "symfony/dependency-injection", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", @@ -3553,7 +3553,7 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", @@ -3632,16 +3632,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "85eaf6a8ec915487abac52e133efc4a268204428" + "reference": "63353a71073faf08f62caab4e6889b06a787f07b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/85eaf6a8ec915487abac52e133efc4a268204428", - "reference": "85eaf6a8ec915487abac52e133efc4a268204428", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/63353a71073faf08f62caab4e6889b06a787f07b", + "reference": "63353a71073faf08f62caab4e6889b06a787f07b", "shasum": "" }, "require": { @@ -3691,11 +3691,11 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-02-14T14:11:10+00:00" + "time": "2018-04-06T07:35:43+00:00" }, { "name": "symfony/expression-language", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", @@ -3745,7 +3745,7 @@ }, { "name": "symfony/filesystem", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3794,16 +3794,16 @@ }, { "name": "symfony/finder", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "c72995d9f5999b3fcdd8660c0c9690243252e1e1" + "reference": "ca27c02b7a3fef4828c998c2ff9ba7aae1641c49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/c72995d9f5999b3fcdd8660c0c9690243252e1e1", - "reference": "c72995d9f5999b3fcdd8660c0c9690243252e1e1", + "url": "https://api.github.com/repos/symfony/finder/zipball/ca27c02b7a3fef4828c998c2ff9ba7aae1641c49", + "reference": "ca27c02b7a3fef4828c998c2ff9ba7aae1641c49", "shasum": "" }, "require": { @@ -3839,7 +3839,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-04-02T09:52:41+00:00" + "time": "2018-04-04T05:10:37+00:00" }, { "name": "symfony/flex", @@ -3889,16 +3889,16 @@ }, { "name": "symfony/form", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "5a3f505b256a65978d1d4f924ed53ceb503d1d5f" + "reference": "8605af5a9181d44637de5706e32d8ab9caee799e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/5a3f505b256a65978d1d4f924ed53ceb503d1d5f", - "reference": "5a3f505b256a65978d1d4f924ed53ceb503d1d5f", + "url": "https://api.github.com/repos/symfony/form/zipball/8605af5a9181d44637de5706e32d8ab9caee799e", + "reference": "8605af5a9181d44637de5706e32d8ab9caee799e", "shasum": "" }, "require": { @@ -3965,20 +3965,20 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2018-04-02T10:05:09+00:00" + "time": "2018-04-06T07:35:43+00:00" }, { "name": "symfony/framework-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "a8e73bb539054efdca59a4ec0d3af99635812505" + "reference": "3571d235434b566aea39d8f8bfe38860344fd9a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a8e73bb539054efdca59a4ec0d3af99635812505", - "reference": "a8e73bb539054efdca59a4ec0d3af99635812505", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/3571d235434b566aea39d8f8bfe38860344fd9a3", + "reference": "3571d235434b566aea39d8f8bfe38860344fd9a3", "shasum": "" }, "require": { @@ -4079,11 +4079,11 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2018-04-02T09:52:41+00:00" + "time": "2018-04-04T18:24:59+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", @@ -4136,16 +4136,16 @@ }, { "name": "symfony/http-kernel", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "38337d03a554a2b0e9f553d368723692b7c04a8f" + "reference": "6dd620d96d64456075536ffe3c6c4658dd689021" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/38337d03a554a2b0e9f553d368723692b7c04a8f", - "reference": "38337d03a554a2b0e9f553d368723692b7c04a8f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6dd620d96d64456075536ffe3c6c4658dd689021", + "reference": "6dd620d96d64456075536ffe3c6c4658dd689021", "shasum": "" }, "require": { @@ -4218,11 +4218,11 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-04-03T06:20:33+00:00" + "time": "2018-04-06T16:25:03+00:00" }, { "name": "symfony/inflector", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/inflector.git", @@ -4279,7 +4279,7 @@ }, { "name": "symfony/intl", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", @@ -4354,7 +4354,7 @@ }, { "name": "symfony/monolog-bridge", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", @@ -4483,7 +4483,7 @@ }, { "name": "symfony/options-resolver", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -4742,7 +4742,7 @@ }, { "name": "symfony/process", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -4791,7 +4791,7 @@ }, { "name": "symfony/property-access", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", @@ -4858,7 +4858,7 @@ }, { "name": "symfony/property-info", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", @@ -4982,16 +4982,16 @@ }, { "name": "symfony/routing", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ca780c838046bfef4a6fd50284ae71a5d1f1a8b2" + "reference": "0663036dd57dbfd4e9ff29f75bbd5dd3253ebe71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ca780c838046bfef4a6fd50284ae71a5d1f1a8b2", - "reference": "ca780c838046bfef4a6fd50284ae71a5d1f1a8b2", + "url": "https://api.github.com/repos/symfony/routing/zipball/0663036dd57dbfd4e9ff29f75bbd5dd3253ebe71", + "reference": "0663036dd57dbfd4e9ff29f75bbd5dd3253ebe71", "shasum": "" }, "require": { @@ -5056,20 +5056,20 @@ "uri", "url" ], - "time": "2018-04-02T09:52:41+00:00" + "time": "2018-04-04T13:50:32+00:00" }, { "name": "symfony/security", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/security.git", - "reference": "3e08200cc978c71d2cfdbc9500db4f2b65bdd28f" + "reference": "6c15e6b36dfa873c9798e41cac5937b61d0eb3fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security/zipball/3e08200cc978c71d2cfdbc9500db4f2b65bdd28f", - "reference": "3e08200cc978c71d2cfdbc9500db4f2b65bdd28f", + "url": "https://api.github.com/repos/symfony/security/zipball/6c15e6b36dfa873c9798e41cac5937b61d0eb3fe", + "reference": "6c15e6b36dfa873c9798e41cac5937b61d0eb3fe", "shasum": "" }, "require": { @@ -5133,20 +5133,20 @@ ], "description": "Symfony Security Component", "homepage": "https://symfony.com", - "time": "2018-04-02T14:35:51+00:00" + "time": "2018-04-06T07:35:43+00:00" }, { "name": "symfony/security-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "4edcb55716cdbbb26783313f34046ada778c87c5" + "reference": "dfabecc1fd3e626e676edef97753a645b4de85a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/4edcb55716cdbbb26783313f34046ada778c87c5", - "reference": "4edcb55716cdbbb26783313f34046ada778c87c5", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/dfabecc1fd3e626e676edef97753a645b4de85a5", + "reference": "dfabecc1fd3e626e676edef97753a645b4de85a5", "shasum": "" }, "require": { @@ -5213,11 +5213,11 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "time": "2018-03-19T22:35:49+00:00" + "time": "2018-04-06T07:35:43+00:00" }, { "name": "symfony/serializer", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", @@ -5388,7 +5388,7 @@ }, { "name": "symfony/templating", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/templating.git", @@ -5443,7 +5443,7 @@ }, { "name": "symfony/translation", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -5511,7 +5511,7 @@ }, { "name": "symfony/twig-bridge", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", @@ -5601,7 +5601,7 @@ }, { "name": "symfony/twig-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", @@ -5674,16 +5674,16 @@ }, { "name": "symfony/validator", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "2f080fe8af133d12bdf3eeb55a9d01e1286f8e24" + "reference": "b9546d78133d6af199ac6625d0d587a2d804f967" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/2f080fe8af133d12bdf3eeb55a9d01e1286f8e24", - "reference": "2f080fe8af133d12bdf3eeb55a9d01e1286f8e24", + "url": "https://api.github.com/repos/symfony/validator/zipball/b9546d78133d6af199ac6625d0d587a2d804f967", + "reference": "b9546d78133d6af199ac6625d0d587a2d804f967", "shasum": "" }, "require": { @@ -5754,11 +5754,11 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-04-02T09:52:41+00:00" + "time": "2018-04-06T07:35:43+00:00" }, { "name": "symfony/web-link", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", @@ -5829,7 +5829,7 @@ }, { "name": "symfony/yaml", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -6812,7 +6812,7 @@ }, { "name": "symfony/browser-kit", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -6869,7 +6869,7 @@ }, { "name": "symfony/css-selector", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -6922,7 +6922,7 @@ }, { "name": "symfony/debug-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", @@ -7017,7 +7017,7 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -7073,7 +7073,7 @@ }, { "name": "symfony/dotenv", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", @@ -7130,16 +7130,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "cdfbf48b17ca56bb7fbcb4126f949665e5f426a3" + "reference": "e82f3f46384482f2a7dab5f00c58a36b9726bde9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/cdfbf48b17ca56bb7fbcb4126f949665e5f426a3", - "reference": "cdfbf48b17ca56bb7fbcb4126f949665e5f426a3", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e82f3f46384482f2a7dab5f00c58a36b9726bde9", + "reference": "e82f3f46384482f2a7dab5f00c58a36b9726bde9", "shasum": "" }, "require": { @@ -7192,7 +7192,7 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", - "time": "2018-03-22T13:42:36+00:00" + "time": "2018-04-04T18:24:59+00:00" }, { "name": "symfony/polyfill-php72", @@ -7279,7 +7279,7 @@ }, { "name": "symfony/stopwatch", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -7328,16 +7328,16 @@ }, { "name": "symfony/var-dumper", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "f9b257f2009c7f09166bcfd3d3d96455741ed371" + "reference": "e1b4d008100f4d203cc38b0d793ad6252d8d8af0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f9b257f2009c7f09166bcfd3d3d96455741ed371", - "reference": "f9b257f2009c7f09166bcfd3d3d96455741ed371", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e1b4d008100f4d203cc38b0d793ad6252d8d8af0", + "reference": "e1b4d008100f4d203cc38b0d793ad6252d8d8af0", "shasum": "" }, "require": { @@ -7393,20 +7393,20 @@ "debug", "dump" ], - "time": "2018-04-03T05:24:00+00:00" + "time": "2018-04-04T05:10:37+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "4d709edab20fdfc889a5c9a2b530453e4a4b8127" + "reference": "4f6a1f77120b5e4b37c59db34a8dc0a4d3df5cf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4d709edab20fdfc889a5c9a2b530453e4a4b8127", - "reference": "4d709edab20fdfc889a5c9a2b530453e4a4b8127", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4f6a1f77120b5e4b37c59db34a8dc0a4d3df5cf0", + "reference": "4f6a1f77120b5e4b37c59db34a8dc0a4d3df5cf0", "shasum": "" }, "require": { @@ -7459,11 +7459,11 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2018-04-02T09:52:41+00:00" + "time": "2018-04-04T13:50:32+00:00" }, { "name": "symfony/web-server-bundle", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/symfony/web-server-bundle.git", From 6368e3d75c6e2f136433463e1501fa947356cb5b Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 7 Apr 2018 11:24:32 +0200 Subject: [PATCH 14/24] fix unit tests --- tests/Form/Model/CommentDataTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Form/Model/CommentDataTest.php b/tests/Form/Model/CommentDataTest.php index ce072ba..bb3c231 100644 --- a/tests/Form/Model/CommentDataTest.php +++ b/tests/Form/Model/CommentDataTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Form\Model; use App\Entity\Comment; +use App\Entity\Submission; use App\Entity\User; use App\Form\Model\CommentData; use PHPUnit\Framework\MockObject\MockObject; @@ -24,10 +25,14 @@ public static function setUpBeforeClass() { protected function setUp() { $this->comment = $this->getMockBuilder(Comment::class) - ->setMethods(['getUser']) + ->setMethods(['getSubmission', 'getUser']) ->disableOriginalConstructor() ->getMock(); + $this->comment + ->method('getSubmission') + ->willReturn($this->createMock(Submission::class)); + $this->comment ->method('getUser') ->willReturn($this->createMock(User::class)); From 23d7e675e4c640922057dcc1b50cb7f9ebc8970b Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 7 Apr 2018 18:25:21 +0200 Subject: [PATCH 15/24] test Votable/minor refactorings for voting stuff --- src/Entity/Comment.php | 2 +- src/Entity/CommentVote.php | 2 +- src/Entity/Submission.php | 2 +- src/Entity/SubmissionVote.php | 2 +- src/Entity/Votable.php | 4 +- src/Entity/Vote.php | 17 ++--- tests/Entity/VotableTest.php | 119 ++++++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 tests/Entity/VotableTest.php diff --git a/src/Entity/Comment.php b/src/Entity/Comment.php index fbe1341..9fe6b15 100644 --- a/src/Entity/Comment.php +++ b/src/Entity/Comment.php @@ -210,7 +210,7 @@ protected function createVote(User $user, ?string $ip, int $choice): Vote { /** * {@inheritdoc} */ - public function vote(User $user, ?string $ip, int $choice) { + public function vote(User $user, ?string $ip, int $choice): void { if ($this->submission->getForum()->userIsBanned($user)) { throw new BannedFromForumException(); } diff --git a/src/Entity/CommentVote.php b/src/Entity/CommentVote.php index 37e532e..3901ffa 100644 --- a/src/Entity/CommentVote.php +++ b/src/Entity/CommentVote.php @@ -25,7 +25,7 @@ class CommentVote extends Vote { */ private $comment; - public function __construct(User $user, ?string $ip, $choice, Comment $comment) { + public function __construct(User $user, ?string $ip, int $choice, Comment $comment) { parent::__construct($user, $ip, $choice); $this->comment = $comment; diff --git a/src/Entity/Submission.php b/src/Entity/Submission.php index 3e936ec..7f82fb4 100644 --- a/src/Entity/Submission.php +++ b/src/Entity/Submission.php @@ -270,7 +270,7 @@ protected function createVote(User $user, ?string $ip, int $choice): Vote { /** * {@inheritdoc} */ - public function vote(User $user, ?string $ip, int $choice) { + public function vote(User $user, ?string $ip, int $choice): void { if ($this->forum->userIsBanned($user)) { throw new BannedFromForumException(); } diff --git a/src/Entity/SubmissionVote.php b/src/Entity/SubmissionVote.php index 402175b..8204f40 100644 --- a/src/Entity/SubmissionVote.php +++ b/src/Entity/SubmissionVote.php @@ -28,7 +28,7 @@ class SubmissionVote extends Vote { /** * {@inheritdoc} */ - public function __construct(User $user, ?string $ip, $choice, Submission $submission) { + public function __construct(User $user, ?string $ip, int $choice, Submission $submission) { parent::__construct($user, $ip, $choice); $this->submission = $submission; diff --git a/src/Entity/Votable.php b/src/Entity/Votable.php index 4050933..8eba94d 100644 --- a/src/Entity/Votable.php +++ b/src/Entity/Votable.php @@ -35,7 +35,7 @@ abstract protected function createVote(User $user, ?string $ip, int $choice): Vo * * @throws \InvalidArgumentException if the vote is not a VOTE_* constant */ - public function vote(User $user, ?string $ip, int $choice) { + public function vote(User $user, ?string $ip, int $choice): void { $vote = $this->getUserVote($user); if ($choice === self::VOTE_UP || $choice === self::VOTE_DOWN) { @@ -100,7 +100,7 @@ private function getUserVote(User $user): ?Vote { * collection in advance speeds up things considerably when there are * multiple entities. */ - private function hydrateVoteCollection() { + private function hydrateVoteCollection(): void { $this->getVotes()->getValues(); } } diff --git a/src/Entity/Vote.php b/src/Entity/Vote.php index a1b8155..a084757 100644 --- a/src/Entity/Vote.php +++ b/src/Entity/Vote.php @@ -52,8 +52,10 @@ abstract class Vote { * @param User $user * @param string|null $ip * @param bool|int $choice + * + * @throws \InvalidArgumentException if $choice is bad */ - public function __construct(User $user, ?string $ip, $choice) { + public function __construct(User $user, ?string $ip, int $choice) { $this->timestamp = new \DateTime('@'.time()); $this->user = $user; $this->setIp($ip); // must be after $this->user is declared @@ -69,13 +71,12 @@ public function getChoice(): int { } /** - * @param int|bool $choice true/Votable::VOTE_UP = upvote, - * false/Votable::VOTE_DOWN = downvote + * @param int $choice one of Votable::VOTE_UP or Votable::VOTE_DOWN + * + * @throws \InvalidArgumentException if $choice isn't a valid parameter */ - public function setChoice($choice) { - if (is_bool($choice)) { - $this->upvote = $choice; - } elseif ($choice === Votable::VOTE_UP || $choice === Votable::VOTE_DOWN) { + public function setChoice(int $choice): void { + if ($choice === Votable::VOTE_UP || $choice === Votable::VOTE_DOWN) { $this->upvote = $choice === Votable::VOTE_UP; } elseif ($choice === Votable::VOTE_RETRACT) { throw new \InvalidArgumentException('A vote entity cannot have a "retracted" status'); @@ -96,7 +97,7 @@ public function getIp(): ?string { return $this->ip; } - public function setIp(?string $ip) { + public function setIp(?string $ip): void { if ($ip !== null && !filter_var($ip, FILTER_VALIDATE_IP)) { throw new \InvalidArgumentException('Bad IP address'); } diff --git a/tests/Entity/VotableTest.php b/tests/Entity/VotableTest.php new file mode 100644 index 0000000..a88bc9a --- /dev/null +++ b/tests/Entity/VotableTest.php @@ -0,0 +1,119 @@ +votable = $this->createVotable(); + } + + public function testVotableScores(): void { + $votable = $this->createVotable(); + $user = $this->createMock(User::class); + + $this->assertEquals(0, $votable->getNetScore()); + $this->assertEquals(0, $votable->getUpvotes()); + $this->assertEquals(0, $votable->getDownvotes()); + + $votable->vote($user, null, Votable::VOTE_UP); + + $this->assertEquals(1, $votable->getNetScore()); + $this->assertEquals(1, $votable->getUpvotes()); + $this->assertEquals(0, $votable->getDownvotes()); + + $votable->vote($user, null, Votable::VOTE_DOWN); + + $this->assertEquals(-1, $votable->getNetScore()); + $this->assertEquals(0, $votable->getUpvotes()); + $this->assertEquals(1, $votable->getDownvotes()); + } + + public function testVoteCollectionHasCorrectProperties(): void { + $user = $this->createMock(User::class); + + $this->votable->vote($user, null, Votable::VOTE_UP); + $this->assertEquals(Votable::USER_UPVOTED, $this->votable->getVotes()->first()->getChoice()); + $this->assertCount(1, $this->votable->getVotes()); + + $this->votable->vote($user, null, Votable::VOTE_DOWN); + $this->assertEquals(Votable::USER_DOWNVOTED, $this->votable->getVotes()->first()->getChoice()); + $this->assertCount(1, $this->votable->getVotes()); + + $this->votable->vote($user, null, Votable::VOTE_RETRACT); + $this->assertCount(0, $this->votable->getVotes()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCannotGiveIncorrectVote(): void { + $user = $this->createMock(User::class); + + $this->votable->vote($user, null, 69); + } + + public function testGetUserVote(): void { + $user1 = $this->createMock(User::class); + $this->votable->vote($user1, null, Votable::VOTE_UP); + + $user2 = $this->createMock(User::class); + $this->votable->vote($user2, null, Votable::VOTE_DOWN); + + $user3 = $this->createMock(User::class); + + $this->assertEquals(Votable::USER_UPVOTED, $this->votable->getUserChoice($user1)); + $this->assertEquals(Votable::USER_DOWNVOTED, $this->votable->getUserChoice($user2)); + $this->assertEquals(Votable::USER_NO_VOTE, $this->votable->getUserChoice($user3)); + } + + /** + * @doesNotPerformAssertions + */ + public function testAcceptsWellFormedIpAddresses(): void { + $user = $this->createMock(User::class); + $this->votable->vote($user, '127.0.4.20', Votable::VOTE_UP); + $this->votable->vote($user, '::69', Votable::VOTE_UP); + $this->votable->vote($user, null, Votable::VOTE_UP); + } + + public function testThrowsExceptionOnBadIpAddress(): void { + $user = $this->createMock(User::class); + + $this->expectException(\InvalidArgumentException::class); + + $this->votable->vote($user, 'poop', Votable::VOTE_UP); + } + + /** + * @return Votable + */ + private function createVotable(): Votable { + return new class() extends Votable { + private $votes; + + public function __construct() { + $this->votes = new ArrayCollection(); + } + + public function getVotes(): Collection { + return $this->votes; + } + + protected function createVote(User $user, ?string $ip, int $choice): Vote { + return new class($user, $ip, $choice) extends Vote {}; + } + }; + } +} From 049b378e5ae98330c9f5533cf15466d96bb33e54 Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 7 Apr 2018 19:25:50 +0200 Subject: [PATCH 16/24] give AjaxListener a proper unit test --- tests/EventListener/AjaxListenerTest.php | 112 ++++++++++++++++++++--- 1 file changed, 100 insertions(+), 12 deletions(-) diff --git a/tests/EventListener/AjaxListenerTest.php b/tests/EventListener/AjaxListenerTest.php index 7bf66d6..1b595ab 100644 --- a/tests/EventListener/AjaxListenerTest.php +++ b/tests/EventListener/AjaxListenerTest.php @@ -2,26 +2,114 @@ namespace App\Tests\EventListener; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use App\EventListener\AjaxListener; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Serializer\Serializer; /** * @covers \App\EventListener\AjaxListener */ -class AjaxListenerTest extends WebTestCase { - public function test403sOnAuthenticationFailure() { - $client = $this->createClient([], [ - 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', - ]); +class AjaxListenerTest extends KernelTestCase { + /** + * @var Serializer + */ + private $serializer; - $client->request('POST', '/cv/1.json'); + /** + * @var AjaxListener + */ + private $listener; - $this->assertSame(403, $client->getResponse()->getStatusCode()); + protected function setUp() { + static::bootKernel(); + + $this->serializer = self::$kernel->getContainer()->get('serializer'); + $this->listener = new AjaxListener($this->serializer); } - public function testRedirectsToLoginWithoutXhr() { - $client = $this->createClient(); - $client->request('POST', '/cv/1.json'); - $this->assertTrue($client->getResponse()->isRedirect()); + public function testDoesNotSetResponseOnNotXhrRequests(): void { + $request = new Request(); + $event = $this->createEvent($request, new AccessDeniedException()); + + $this->listener->onKernelException($event); + + $this->assertNull($event->getResponse()); + } + + /** + * @dataProvider fourOhThreeOnExceptionWithSerializedBodyProvider + */ + public function test403sOnExceptionWithSerializedBody(Request $request, \Exception $e): void { + $event = $this->createEvent($request, $e); + + $this->listener->onKernelException($event); + + $this->assertInstanceOf(Response::class, $event->getResponse()); + $this->assertEquals(403, $event->getResponse()->getStatusCode()); + $this->assertEquals( + $this->serializer->serialize( + ['error' => $e->getMessage()], + $request->getRequestFormat() + ), + $event->getResponse()->getContent() + ); + } + + /** + * @dataProvider fourOhThreeOnExceptionWithPlainBodyProvider + */ + public function test403sOnExceptionWithPlainBody(Request $request, \Exception $e): void { + $event = $this->createEvent($request, $e); + + $this->listener->onKernelException($event); + + $this->assertInstanceOf(Response::class, $event->getResponse()); + $this->assertEquals(403, $event->getResponse()->getStatusCode()); + $this->assertEquals($e->getMessage(), $event->getResponse()->getContent()); + } + + public function fourOhThreeOnExceptionWithSerializedBodyProvider() { + $request = new Request(); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + $request->setRequestFormat('json'); + $exception = new AuthenticationException('foo'); + + yield [$request, $exception]; + + $request->setRequestFormat('xml'); + + yield [$request, $exception]; + + $exception = new AccessDeniedException('aaa'); + + yield [$request, $exception]; + } + + public function fourOhThreeOnExceptionWithPlainBodyProvider() { + $request = new Request(); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + $exception = new AuthenticationException('sheep'); + + yield [$request, $exception]; + + $exception = new AccessDeniedException('cow'); + + yield [$request, $exception]; + } + + private function createEvent(Request $request, \Exception $e): GetResponseForExceptionEvent { + return new GetResponseForExceptionEvent( + self::$kernel, + $request, + HttpKernelInterface::MASTER_REQUEST, + $e + ); } } From d4325c10fb27a8c7e00f747d9e02ebf204cd1b6c Mon Sep 17 00:00:00 2001 From: Emma Date: Sun, 8 Apr 2018 04:33:54 +0200 Subject: [PATCH 17/24] completely redo the way submissions are fetched adds: - keyset pagination (fast!) - ability to limit to forums/users, and exclude forums/users - reintroduced top/controversial sort modes - adds a new 'most commented' sort mode removes: - previous button (no one cared anyway) --- config/app_routes/forum.yaml | 24 +- config/app_routes/forum_category.yaml | 6 +- config/app_routes/front.yaml | 43 +-- config/services.yaml | 1 + src/Controller/AbstractController.php | 6 + src/Controller/ForumCategoryController.php | 9 +- src/Controller/ForumController.php | 41 +-- src/Controller/FrontController.php | 81 ++++-- src/Entity/Submission.php | 4 + .../Submission/NoSubmissionsException.php | 19 ++ src/Repository/Submission/SubmissionPager.php | 109 ++++++++ src/Repository/SubmissionRepository.php | 245 +++++++++++------- templates/_includes/meta_pagination.html.twig | 12 +- templates/_includes/pagination.html.twig | 4 +- templates/_macros/feed_macros.xml.twig | 6 +- templates/front/base.html.twig | 8 +- templates/submission/_macros.html.twig | 4 +- tests/ApplicationAvailabilityTest.php | 67 +++-- translations/messages.en.yml | 1 + 19 files changed, 478 insertions(+), 212 deletions(-) create mode 100644 src/Repository/Submission/NoSubmissionsException.php create mode 100644 src/Repository/Submission/SubmissionPager.php diff --git a/config/app_routes/forum.yaml b/config/app_routes/forum.yaml index adc578d..90915fc 100644 --- a/config/app_routes/forum.yaml +++ b/config/app_routes/forum.yaml @@ -1,24 +1,30 @@ multi: controller: App\Controller\ForumController::multi - defaults: { sortBy: hot, page: 1 } - path: /f/{names}/{sortBy}/{page} + defaults: { sortBy: hot } + path: /f/{names}/{sortBy} requirements: names: '(?:\w{3,25}\+){1,70}\w{3,25}' - sortBy: hot|new + sortBy: "%submission_sort_modes%" forum: controller: App\Controller\ForumController::front - defaults: { sortBy: hot, page: 1 } - path: /f/{forum_name}/{sortBy}/{page} + defaults: { sortBy: hot } + path: /f/{forum_name}/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } forum_feed: controller: App\Controller\ForumController::feed - defaults: { sortBy: hot, page: 1, _format: xml } - path: /f/{forum_name}/{sortBy}/{page}.atom + defaults: { sortBy: hot, _format: xml } + path: /f/{forum_name}/{sortBy}.atom methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } + +forum_feed_legacy_redirect: + controller: FrameworkBundle:Redirect:redirect + defaults: { route: forum_feed, ignoreAttributes: [page] } + path: /f/{forum_name}/{sortBy}/{page}.atom + requirements: { page: \d+ } edit_forum: controller: App\Controller\ForumController::editForum diff --git a/config/app_routes/forum_category.yaml b/config/app_routes/forum_category.yaml index 795d12f..184bf0d 100644 --- a/config/app_routes/forum_category.yaml +++ b/config/app_routes/forum_category.yaml @@ -6,10 +6,10 @@ manage_forum_categories: forum_category: controller: App\Controller\ForumCategoryController::category - defaults: { sortBy: hot, page: 1 } - path: /c/{name}/{sortBy}/{page} + defaults: { sortBy: hot } + path: /c/{name}/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } create_forum_category: controller: App\Controller\ForumCategoryController::create diff --git a/config/app_routes/front.yaml b/config/app_routes/front.yaml index 625c6ab..bd4d02d 100644 --- a/config/app_routes/front.yaml +++ b/config/app_routes/front.yaml @@ -1,41 +1,48 @@ front: controller: App\Controller\FrontController::front - defaults: { sortBy: hot, page: 1 } - path: /{sortBy}/{page} + defaults: { sortBy: hot } + path: /{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } featured: controller: App\Controller\FrontController::featured - defaults: { sortBy: hot, page: 1} - path: /featured/{sortBy}/{page} + defaults: { sortBy: hot } + path: /featured/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } subscribed: controller: App\Controller\FrontController::subscribed - defaults: { sortBy: hot, page: 1 } - path: /subscribed/{sortBy}/{page} + defaults: { sortBy: hot } + path: /subscribed/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } all: controller: App\Controller\FrontController::all - defaults: { sortBy: hot, page: 1 } - path: /all/{sortBy}/{page} + defaults: { sortBy: hot } + path: /all/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } moderated: controller: App\Controller\FrontController::moderated - defaults: { sortBy: hot, page: 1 } - path: /moderated/{sortBy}/{page} + defaults: { sortBy: hot } + path: /moderated/{sortBy} methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } featured_feed: controller: App\Controller\FrontController::featuredFeed - defaults: { sortBy: hot, page: 1, _format: xml } - path: /featured/{sortBy}/{page}.atom + defaults: { sortBy: hot, _format: xml } + path: /featured/{sortBy}.atom methods: [GET] - requirements: { sortBy: hot|new, page: \d+ } + requirements: { sortBy: "%submission_sort_modes%" } + +featured_feed_legacy_redirect: + controller: FrameworkBundle:Redirect:redirect + defaults: { route: featured_feed, ignoreAttributes: true } + methods: [GET] + path: /featured/{sortBy}/{_page}.atom + requirements: { sortBy: "%submission_sort_modes%", page: \d+ } diff --git a/config/services.yaml b/config/services.yaml index 9d66a0b..96601f0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -6,6 +6,7 @@ parameters: uuid_regex: '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' wiki_page_regex: '[A-Za-z][A-Za-z0-9_-]*(/[A-Za-z][A-Za-z0-9_-]*)*' env(APP_ENABLE_WEBHOOKS): false + submission_sort_modes: hot|new|top|controversial|most_commented services: _defaults: diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index ce358a8..4874233 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -2,10 +2,16 @@ namespace App\Controller; +use App\Repository\Submission\SubmissionPager; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; abstract class AbstractController extends BaseAbstractController { + protected function submissionPage(string $sortBy, Request $request): array { + return SubmissionPager::getParamsFromRequest($sortBy, $request); + } + protected function validateCsrf(string $id, string $token) { if (!$this->isCsrfTokenValid($id, $token)) { throw new BadRequestHttpException('Invalid CSRF token'); diff --git a/src/Controller/ForumCategoryController.php b/src/Controller/ForumCategoryController.php index 0af013c..b1f11c1 100644 --- a/src/Controller/ForumCategoryController.php +++ b/src/Controller/ForumCategoryController.php @@ -16,13 +16,16 @@ class ForumCategoryController extends AbstractController { public function category( ForumCategory $category, - string $sortBy, int $page, + string $sortBy, ForumRepository $fr, - SubmissionRepository $sr + SubmissionRepository $sr, + Request $request ): Response { $forums = $fr->findForumsInCategory($category); - $submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page); + $submissions = $sr->findSubmissions($sortBy, [ + 'forums' => array_keys($forums), + ], $this->submissionPage($sortBy, $request)); return $this->render('forum_category/category.html.twig', [ 'category' => $category, diff --git a/src/Controller/ForumController.php b/src/Controller/ForumController.php index 49efcce..8516aaf 100644 --- a/src/Controller/ForumController.php +++ b/src/Controller/ForumController.php @@ -34,27 +34,34 @@ * @Entity("user", expr="repository.findOneOrRedirectToCanonical(username, 'username')") */ final class ForumController extends AbstractController { + /** + * @var SubmissionRepository + */ + private $submissions; + /** * @var bool */ private $enableWebhooks; - public function __construct(bool $enableWebhooks) { + public function __construct(SubmissionRepository $submissions, bool $enableWebhooks) { + $this->submissions = $submissions; $this->enableWebhooks = $enableWebhooks; } /** * Show the front page of a given forum. * - * @param SubmissionRepository $sr - * @param Forum $forum - * @param string $sortBy - * @param int $page + * @param Forum $forum + * @param string $sortBy * * @return Response */ - public function front(SubmissionRepository $sr, Forum $forum, string $sortBy, int $page) { - $submissions = $sr->findForumSubmissions($forum, $sortBy, $page); + public function front(Forum $forum, string $sortBy, Request $request): Response { + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => [$forum->getId()], + 'stickies' => true, + ], $this->submissionPage($sortBy, $request)); return $this->render('forum/forum.html.twig', [ 'forum' => $forum, @@ -63,8 +70,7 @@ public function front(SubmissionRepository $sr, Forum $forum, string $sortBy, in ]); } - public function multi(ForumRepository $fr, SubmissionRepository $sr, - string $names, string $sortBy, int $page) { + public function multi(ForumRepository $fr, string $names, string $sortBy, Request $request) { $names = preg_split('/[^\w]+/', $names, -1, PREG_SPLIT_NO_EMPTY); $names = array_map(Forum::class.'::normalizeName', $names); $names = $fr->findForumNames($names); @@ -73,7 +79,9 @@ public function multi(ForumRepository $fr, SubmissionRepository $sr, throw $this->createNotFoundException('no such forums'); } - $submissions = $sr->findFrontPageSubmissions($names, $sortBy, $page); + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => array_keys($names), + ], $this->submissionPage($sortBy, $request)); return $this->render('forum/multi.html.twig', [ 'forums' => $names, @@ -148,17 +156,18 @@ public function editForum(Request $request, Forum $forum, EntityManager $em) { } /** - * @param Forum $forum - * @param SubmissionRepository $sr - * @param string $sortBy - * @param int $page + * @param Forum $forum + * @param string $sortBy + * @param Request $request * * @return Response */ - public function feed(Forum $forum, SubmissionRepository $sr, string $sortBy, int $page) { + public function feed(Forum $forum, string $sortBy, Request $request) { return $this->render('forum/feed.xml.twig', [ 'forum' => $forum, - 'submissions' => $sr->findForumSubmissions($forum, $sortBy, $page), + 'submissions' => $this->submissions->findSubmissions($sortBy, [ + 'forums' => [$forum->getId()], + ], $this->submissionPage($sortBy, $request)), ]); } diff --git a/src/Controller/FrontController.php b/src/Controller/FrontController.php index 549b7e3..34dc192 100644 --- a/src/Controller/FrontController.php +++ b/src/Controller/FrontController.php @@ -5,6 +5,8 @@ use App\Entity\User; use App\Repository\ForumRepository; use App\Repository\SubmissionRepository; +use App\Repository\Submission\SubmissionPager; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** @@ -25,7 +27,25 @@ * instead. */ final class FrontController extends AbstractController { - public function front(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) { + /** + * @var ForumRepository + */ + private $forums; + + /** + * @var SubmissionRepository + */ + private $submissions; + + public function __construct( + ForumRepository $forums, + SubmissionRepository $submissions + ) { + $this->forums = $forums; + $this->submissions = $submissions; + } + + public function front(string $sortBy, Request $request): Response { $user = $this->getUser(); if (!$user instanceof User) { @@ -38,21 +58,24 @@ public function front(ForumRepository $fr, SubmissionRepository $sr, string $sor switch ($listing) { case User::FRONT_SUBSCRIBED: - return $this->subscribed($fr, $sr, $sortBy, $page); + return $this->subscribed($sortBy, $request); case User::FRONT_FEATURED: - return $this->featured($fr, $sr, $sortBy, $page); + return $this->featured($sortBy, $request); case User::FRONT_ALL: - return $this->all($sr, $sortBy, $page); + return $this->all($sortBy, $request); case User::FRONT_MODERATED: - return $this->moderated($fr, $sr, $sortBy, $page); + return $this->moderated($sortBy, $request); default: throw new \InvalidArgumentException('bad front page selection'); } } - public function featured(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) { - $forums = $fr->findFeaturedForumNames(); - $submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page); + public function featured(string $sortBy, Request $request): Response { + $forums = $this->forums->findFeaturedForumNames(); + + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => array_keys($this->forums->findFeaturedForumNames()), + ], $this->submissionPage($sortBy, $request)); return $this->render('front/featured.html.twig', [ 'forums' => $forums, @@ -62,17 +85,19 @@ public function featured(ForumRepository $fr, SubmissionRepository $sr, string $ ]); } - public function subscribed(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) { + public function subscribed(string $sortBy, Request $request): Response { $this->denyAccessUnlessGranted('ROLE_USER'); - $forums = $fr->findSubscribedForumNames($this->getUser()); - $hasSubscriptions = count($forums) > 0; + $forums = $this->forums->findSubscribedForumNames($this->getUser()); + $hasSubscriptions = \count($forums) > 0; if (!$hasSubscriptions) { - $forums = $fr->findFeaturedForumNames(); + $forums = $this->forums->findFeaturedForumNames(); } - $submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page); + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => array_keys($forums), + ], $this->submissionPage($sortBy, $request)); return $this->render('front/subscribed.html.twig', [ 'forums' => $forums, @@ -83,15 +108,9 @@ public function subscribed(ForumRepository $fr, SubmissionRepository $sr, string ]); } - /** - * @param SubmissionRepository $sr - * @param string $sortBy - * @param int $page - * - * @return Response - */ - public function all(SubmissionRepository $sr, string $sortBy, int $page) { - $submissions = $sr->findAllSubmissions($sortBy, $page); + public function all(string $sortBy, Request $request): Response { + $submissions = $this->submissions->findSubmissions($sortBy, [], + $this->submissionPage($sortBy, $request)); return $this->render('front/all.html.twig', [ 'listing' => 'all', @@ -100,11 +119,14 @@ public function all(SubmissionRepository $sr, string $sortBy, int $page) { ]); } - public function moderated(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) { + public function moderated(string $sortBy, Request $request): Response { $this->denyAccessUnlessGranted('ROLE_USER'); - $forums = $fr->findModeratedForumNames($this->getUser()); - $submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page); + $forums = $this->forums->findModeratedForumNames($this->getUser()); + + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => array_keys($forums), + ], $this->submissionPage($sortBy, $request)); return $this->render('front/moderated.html.twig', [ 'forums' => $forums, @@ -114,9 +136,12 @@ public function moderated(ForumRepository $fr, SubmissionRepository $sr, string ]); } - public function featuredFeed(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page = 1) { - $forums = $fr->findFeaturedForumNames(); - $submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page); + public function featuredFeed(string $sortBy, Request $request): Response { + $forums = $this->forums->findFeaturedForumNames(); + + $submissions = $this->submissions->findSubmissions($sortBy, [ + 'forums' => array_keys($forums), + ], $this->submissionPage($sortBy, $request)); return $this->render('front/featured.xml.twig', [ 'forums' => $forums, diff --git a/src/Entity/Submission.php b/src/Entity/Submission.php index 7f82fb4..388160a 100644 --- a/src/Entity/Submission.php +++ b/src/Entity/Submission.php @@ -215,6 +215,10 @@ public function getComments(): Collection { return $this->comments; } + public function getCommentCount(): int { + return \count($this->comments); + } + /** * Get top-level comments, ordered by descending net score. * diff --git a/src/Repository/Submission/NoSubmissionsException.php b/src/Repository/Submission/NoSubmissionsException.php new file mode 100644 index 0000000..67b498a --- /dev/null +++ b/src/Repository/Submission/NoSubmissionsException.php @@ -0,0 +1,19 @@ +query->get('next_'.$column); + $type = SubmissionRepository::SORT_COLUMN_TYPES[$column]; + + if ($value === null || !self::valueIsOfType($type, $value)) { + // missing columns - no pagination + return []; + } + + $params[$column] = $value; + } + + // complete pager params + return $params; + } + + /** + * @param Submission[]|iterable $submissions List of submissions, including + * one more than $maxPerPage to + * tell if there's a next page + * @param int $maxPerPage + * @param string $sortBy property to use for pagination + */ + public function __construct(iterable $submissions, int $maxPerPage, string $sortBy) { + if (!isset(SubmissionRepository::SORT_COLUMN_MAP[$sortBy])) { + throw new \InvalidArgumentException("Invalid sort mode '$sortBy'"); + } + + $count = 0; + + foreach ($submissions as $submission) { + if (++$count > $maxPerPage) { + foreach (SubmissionRepository::SORT_COLUMN_MAP[$sortBy] as $column) { + $accessor = $this->columnNameToAccessor($column); + $value = $submission->{$accessor}(); + + $this->nextPageParams['next_'.$column] = $value; + } + + break; + } + + $this->submissions[] = $submission; + } + } + + public function getIterator() { + return new \ArrayIterator($this->submissions); + } + + public function hasNextPage(): bool { + return (bool) $this->nextPageParams; + } + + /** + * @throws \BadMethodCallException if there is no next page + */ + public function getNextPageParams(): array { + if (!$this->hasNextPage()) { + throw new \BadMethodCallException('There is no next page'); + } + + return $this->nextPageParams; + } + + private function columnNameToAccessor(string $columnName): string { + return 'get'.str_replace('_', '', ucwords($columnName, '_')); + } + + private static function valueIsOfType(string $type, $value): bool { + switch ($type) { + case 'integer': + return ctype_digit($value) && \is_int(+$value) && + $value >= 0x80000000 && $value <= 0x7fffffff; + case 'bigint': + // if this causes problems on 32-bit systems, the site operators + // deserved it. + return ctype_digit($value) && \is_int(+$value); + default: + throw new \InvalidArgumentException("Unexpected type '$type'"); + } + } +} diff --git a/src/Repository/SubmissionRepository.php b/src/Repository/SubmissionRepository.php index 5f2eeb6..e52ded7 100644 --- a/src/Repository/SubmissionRepository.php +++ b/src/Repository/SubmissionRepository.php @@ -2,114 +2,196 @@ namespace App\Repository; -use App\Entity\Forum; use App\Entity\Submission; -use App\Utils\PrependOrderBy; +use App\Repository\Submission\NoSubmissionsException; +use App\Repository\Submission\SubmissionPager; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\QueryBuilder; -use Pagerfanta\Adapter\DoctrineORMAdapter; -use Pagerfanta\Pagerfanta; +use Doctrine\DBAL\Query\QueryBuilder; class SubmissionRepository extends ServiceEntityRepository { - const MAX_PER_PAGE = 25; + public const SORT_HOT = 'hot'; + public const SORT_NEW = 'new'; + public const SORT_TOP = 'top'; + public const SORT_CONTROVERSIAL = 'controversial'; + public const SORT_MOST_COMMENTED = 'most_commented'; + + /** + * `$sortBy` -> ordered column name mapping + * + * @var array[] + */ + public const SORT_COLUMN_MAP = [ + self::SORT_HOT => ['ranking', 'id'], + self::SORT_NEW => ['id'], + self::SORT_TOP => ['net_score', 'id'], + self::SORT_CONTROVERSIAL => ['downvotes', 'id'], + self::SORT_MOST_COMMENTED => ['comment_count', 'id'], + ]; + + public const SORT_COLUMN_TYPES = [ + 'ranking' => 'bigint', + 'id' => 'bigint', + 'net_score' => 'integer', + 'downvotes' => 'integer', + 'comment_count' => 'integer', + ]; + + private const MAX_PER_PAGE = 25; + + private const NET_SCORE_JOIN = '('. + 'SELECT submission_id, '. + 'COUNT(*) FILTER (WHERE upvote = TRUE) - '. + 'COUNT(*) FILTER (WHERE upvote = FALSE) AS net_score '. + 'FROM submission_votes '. + 'GROUP BY submission_id'. + ')'; + + // TODO: implement actually useful controversy metric + private const CONTROVERSIAL_JOIN = '('. + 'SELECT submission_id, COUNT(*) AS downvotes '. + 'FROM submission_votes '. + 'WHERE NOT upvote = FALSE '. + 'GROUP BY submission_id'. + ')'; + + private const COMMENT_COUNT_JOIN = '('. + 'SELECT submission_id, COUNT(*) AS comment_count '. + 'FROM comments '. + 'GROUP BY submission_id'. + ')'; public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Submission::class); } /** - * @param string[] $forums array where keys are forum IDs - * @param string $sortBy - * @param int $page + * The amazing submission finder. + * + * @param string $sortBy One of SORT_* constants + * @param array $options An array with the following keys: + *
      + *
    • forums: IDs of forums to restrict + * to. + *
    • excluded_forums: IDs of excluded + * (blacklisted) forums. Blacklisted forums will + * not be displayed even if they are included in + * forums. + *
    • users: IDs of users to restrict to. + *
    • excluded_users: IDs of excluded + * users. Again, blacklisting takes precedence. + *
    • stickies: Put stickies first. + *
    • max_per_page: Self-explanatory. + *
    + * @param array $pager + * + * @return Submission[]|SubmissionPager * - * @return Pagerfanta|Submission[] + * @throws \InvalidArgumentException if $sortBy is bad + * @throws NoSubmissionsException if there are no submissions */ - public function findFrontPageSubmissions(array $forums, string $sortBy, int $page = 1) { - if (isset($forums[0])) { - // make sure $forums is id => forum_name array - throw new \InvalidArgumentException('Keys in $forums must be IDs'); - } + public function findSubmissions(string $sortBy, array $options = [], array $pager = []) { + $maxPerPage = $options['max_per_page'] ?? self::MAX_PER_PAGE; - $qb = $this->findSortedQb($sortBy) - ->where('IDENTITY(s.forum) IN (:forums)') - ->setParameter(':forums', array_keys($forums)); + $rsm = $this->createResultSetMappingBuilder('s'); - $submissions = $this->paginate($qb, $page); + $qb = $this->_em->getConnection()->createQueryBuilder() + ->select($rsm->generateSelectClause()) + ->from('submissions', 's') + ->setMaxResults($maxPerPage + 1); - $this->hydrateAssociations($submissions); + switch ($sortBy) { + case self::SORT_HOT: + case self::SORT_NEW: + break; + case self::SORT_TOP: + $qb->join('s', self::NET_SCORE_JOIN, 'ns', 's.id = ns.submission_id'); + break; + case self::SORT_CONTROVERSIAL: + $qb->join('s', self::CONTROVERSIAL_JOIN, 'cn', 's.id = cn.submission_id'); + break; + case self::SORT_MOST_COMMENTED: + $qb->join('s', self::COMMENT_COUNT_JOIN, 'cc', 's.id = cc.submission_id'); + break; + default: + throw new \InvalidArgumentException("Sort mode '$sortBy' not implemented"); + } - return $submissions; - } + if (!$pager && !empty($options['stickies'])) { + // FIXME: won't work if there are >= $maxPerPage stickies (lol) + $qb->orderBy('sticky', 'DESC'); + } - /** - * @param Forum $forum - * @param string $sortBy - * @param int $page - * - * @return Pagerfanta|Submission[] - */ - public function findForumSubmissions(Forum $forum, string $sortBy, int $page = 1) { - $qb = $this->findSortedQb($sortBy) - ->andWhere('s.forum = :forum') - ->setParameter('forum', $forum); + foreach (self::SORT_COLUMN_MAP[$sortBy] as $column) { + $qb->addOrderBy($column, 'DESC'); + } + + if ($pager) { + $qb->andWhere(sprintf('(%s) <= (:next_%s)', + implode(', ', self::SORT_COLUMN_MAP[$sortBy]), + implode(', :next_', self::SORT_COLUMN_MAP[$sortBy]) + )); - if ($sortBy === 'hot') { - PrependOrderBy::prepend($qb, 's.sticky', 'DESC'); + foreach (self::SORT_COLUMN_MAP[$sortBy] as $column) { + $qb->setParameter('next_'.$column, $pager[$column]); + } } - $submissions = $this->paginate($qb, $page); + self::filterQuery($qb, $options); - $this->hydrateAssociations($submissions); + $results = $this->_em + ->createNativeQuery($qb->getSQL(), $rsm) + ->setParameters($qb->getParameters()) + ->execute(); - return $submissions; - } + if ($pager && \count($results) === 0) { + throw new NoSubmissionsException(); + } - /** - * @param string $sortBy - * @param int $page - * - * @return Pagerfanta|Submission[] - */ - public function findAllSubmissions(string $sortBy, int $page = 1) { - $submissions = $this->paginate($this->findSortedQb($sortBy), $page); + $submissions = new SubmissionPager($results, $maxPerPage, $sortBy); $this->hydrateAssociations($submissions); return $submissions; } - /** - * @param string $sortType one of 'hot' or 'new' - * - * @return QueryBuilder - */ - public function findSortedQb($sortType) { - $qb = $this->createQueryBuilder('s'); - - switch ($sortType) { - case 'hot': - $this->sortByHot($qb); - break; - case 'new': - $this->sortByNewest($qb); - break; - case 'top': - throw new \InvalidArgumentException('Sorting by "top" is no longer supported'); - case 'controversial': - throw new \InvalidArgumentException('Sorting by "controversial" is no longer supported'); - default: - throw new \InvalidArgumentException('Bad sort type'); + private static function filterQuery(QueryBuilder $qb, array $options): void { + if (!empty($options['forums'])) { + /* @noinspection NotOptimalIfConditionsInspection */ + if (!empty($options['excluded_forums'])) { + $options['forums'] = array_diff( + $options['forums'], + $options['excluded_forums'] + ); + } + + $qb->andWhere('s.forum_id IN (:forum_ids)'); + $qb->setParameter('forum_ids', $options['forums']); + } elseif (!empty($options['excluded_forums'])) { + $qb->andWhere('s.forum_id NOT IN (:forum_ids)'); + $qb->setParameter('forum_ids', $options['excluded_forums']); } - return $qb; + if (!empty($options['users'])) { + /* @noinspection NotOptimalIfConditionsInspection */ + if (!empty($options['excluded_users'])) { + $options['users'] = array_diff( + $options['users'], + $options['excluded_users'] + ); + } + + $qb->andWhere('s.user_id IN (:user_ids)'); + $qb->setParameter('user_ids', $options['users']); + } elseif (!empty($options['excluded_users'])) { + $qb->andWhere('s.user_id NOT IN (:user_ids)'); + $qb->setParameter('user_ids', $options['excluded_users']); + } } - public function hydrateAssociations($submissions) { + private function hydrateAssociations(iterable $submissions): void { if ($submissions instanceof \Traversable) { $submissions = iterator_to_array($submissions); - } elseif (!is_array($submissions)) { - throw new \InvalidArgumentException('$submissions must be iterable'); } $this->_em->createQueryBuilder() @@ -144,23 +226,4 @@ public function hydrateAssociations($submissions) { ->getQuery() ->getResult(); } - - private function sortByHot(QueryBuilder $qb) { - $qb->addOrderBy('s.ranking', 'DESC'); - $qb->addOrderBy('s.id', 'DESC'); - } - - private function sortByNewest(QueryBuilder $qb) { - $qb->addOrderBy('s.id', 'DESC'); - } - - private function paginate($query, int $page): Pagerfanta { - // I don't think we need to fetch-join when joined entities aren't - // included in the result. - $pager = new Pagerfanta(new DoctrineORMAdapter($query, false, false)); - $pager->setMaxPerPage(self::MAX_PER_PAGE); - $pager->setCurrentPage($page); - - return $pager; - } } diff --git a/templates/_includes/meta_pagination.html.twig b/templates/_includes/meta_pagination.html.twig index a298d0e..f0327eb 100644 --- a/templates/_includes/meta_pagination.html.twig +++ b/templates/_includes/meta_pagination.html.twig @@ -1,9 +1,15 @@ {% with {attr: app.request.attributes, get: app.request.query.all} %} - {% if pager.hasPreviousPage %} - + {% if pager.hasPreviousPage ?? false %} + {% endif %} {% if pager.hasNextPage %} - + {% endif %} {% endwith %} diff --git a/templates/_includes/pagination.html.twig b/templates/_includes/pagination.html.twig index f903444..9a3f5d4 100644 --- a/templates/_includes/pagination.html.twig +++ b/templates/_includes/pagination.html.twig @@ -1,7 +1,7 @@ {% with { attr: app.request.attributes, get: app.request.query.all, - hasPrev: pager.hasPreviousPage, + hasPrev: pager.hasPreviousPage ?? false, hasNext: pager.hasNextPage, } %} {% if hasPrev or hasNext %} @@ -16,7 +16,7 @@ {% endif %} {% if hasNext %} diff --git a/templates/_macros/feed_macros.xml.twig b/templates/_macros/feed_macros.xml.twig index da7c31d..62258f5 100644 --- a/templates/_macros/feed_macros.xml.twig +++ b/templates/_macros/feed_macros.xml.twig @@ -1,11 +1,7 @@ {% macro pagination(pager) %} {% with { route: app.request.attributes.get('_route'), params: app.request.attributes.get('_route_params') ?? {} } %} {% if pager.hasNextPage %} - - {% endif %} - - {% if pager.hasPreviousPage %} - + {% endif %} {% endwith %} {% endmacro %} diff --git a/templates/front/base.html.twig b/templates/front/base.html.twig index 595ef00..cc5fd93 100644 --- a/templates/front/base.html.twig +++ b/templates/front/base.html.twig @@ -33,7 +33,7 @@ {%- macro submission_filter(choice, sort_by) -%} {% with { active: choice == 'featured' } %}
  • - + {{- 'front.featured'|trans -}}
  • @@ -42,7 +42,7 @@ {% if is_granted('ROLE_USER') %} {% with { active: choice == 'subscribed' } %}
  • - + {{- 'front.subscribed'|trans -}}
  • @@ -51,7 +51,7 @@ {% with { active: choice == 'all' } %}
  • - + {{- 'front.all'|trans -}}
  • @@ -60,7 +60,7 @@ {% if choice == 'moderated' or app.user and app.user.moderatorTokens|length > 0 %} {% with { active: choice == 'moderated' } %}
  • - + {{- 'nav.moderated'|trans -}}
  • diff --git a/templates/submission/_macros.html.twig b/templates/submission/_macros.html.twig index dff6112..4c1b14a 100644 --- a/templates/submission/_macros.html.twig +++ b/templates/submission/_macros.html.twig @@ -10,9 +10,9 @@ {%- macro submission_sort(current) -%} {%- set attr = app.request.attributes -%} {%- spaceless -%} - {%- for type in ['hot', 'new'] -%} + {%- for type in ['hot', 'new', 'top', 'controversial', 'most_commented'] -%}
  • - + {{- ('submissions.sort_by_'~type)|trans -}}
  • diff --git a/tests/ApplicationAvailabilityTest.php b/tests/ApplicationAvailabilityTest.php index db35da9..e8a09ac 100644 --- a/tests/ApplicationAvailabilityTest.php +++ b/tests/ApplicationAvailabilityTest.php @@ -14,7 +14,7 @@ class ApplicationAvailabilityTest extends WebTestCase { * @param string $url */ public function testCanAccessPublicPages($url) { - $client = $this->createClient(); + $client = self::createClient(); $client->request('GET', $url); $this->assertTrue($client->getResponse()->isSuccessful()); @@ -26,7 +26,7 @@ public function testCanAccessPublicPages($url) { * @param string $url */ public function testCanAccessPagesThatNeedAuthentication($url) { - $client = $this->createClient([], [ + $client = self::createClient([], [ 'PHP_AUTH_USER' => 'emma', 'PHP_AUTH_PW' => 'goodshit', ]); @@ -41,7 +41,7 @@ public function testCanAccessPagesThatNeedAuthentication($url) { * @param string $url */ public function testCannotAccessPagesThatNeedAuthenticationWhenNotAuthenticated($url) { - $client = $this->createClient(); + $client = self::createClient(); $client->request('GET', $url); $this->assertTrue($client->getResponse()->isRedirect()); @@ -55,10 +55,12 @@ public function testCannotAccessPagesThatNeedAuthenticationWhenNotAuthenticated( * @param string $url */ public function testRedirectedUrlsGoToExpectedLocation($expectedLocation, $url) { - $client = $this->createClient(); + $client = self::createClient(); $client->followRedirects(); $client->request('GET', $url); + $this->assertTrue($client->getResponse()->isSuccessful()); + $this->assertEquals( "http://localhost{$expectedLocation}", $client->getCrawler()->getUri() @@ -73,29 +75,37 @@ public function publicUrlProvider() { yield ['/']; yield ['/hot']; yield ['/new']; - yield ['/hot/1']; - yield ['/new/1']; + yield ['/top']; + yield ['/controversial']; + yield ['/most_commented']; yield ['/all/hot']; yield ['/all/new']; - yield ['/all/hot/1']; - yield ['/all/new/1']; + yield ['/all/top']; + yield ['/all/controversial']; + yield ['/all/most_commented']; yield ['/featured/hot']; yield ['/featured/new']; - yield ['/featured/hot/1']; - yield ['/featured/new/1']; - yield ['/featured/hot/1.atom']; - yield ['/featured/new/1.atom']; + yield ['/featured/top']; + yield ['/featured/controversial']; + yield ['/featured/most_commented']; + yield ['/featured/hot.atom']; + yield ['/featured/new.atom']; + yield ['/featured/top.atom']; + yield ['/featured/controversial.atom']; + yield ['/featured/most_commented.atom']; yield ['/f/news/hot']; yield ['/f/news/new']; - yield ['/f/news/hot/1']; - yield ['/f/news/new/1']; - yield ['/f/news/hot/1.atom']; - yield ['/f/news/new/1.atom']; - yield ['/f/news/1']; + yield ['/f/news/top']; + yield ['/f/news/controversial']; + yield ['/f/news/most_commented']; + yield ['/f/news/hot.atom']; + yield ['/f/news/new.atom']; + yield ['/f/news/top.atom']; + yield ['/f/news/controversial.atom']; + yield ['/f/news/most_commented.atom']; yield ['/f/news/1/comment/1']; yield ['/f/news/bans']; yield ['/f/news/moderation_log']; - yield ['/f/cats/2']; yield ['/forums']; yield ['/forums/by_name']; yield ['/forums/by_title']; @@ -112,16 +122,15 @@ public function publicUrlProvider() { } public function redirectUrlProvider() { - yield ['/f/cats/2', '/f/cats/2/']; yield ['/f/cats', '/f/cats/']; - yield ['/f/cats/2', '/f/CATS/2/']; - yield ['/f/cats/2', '/f/CATS/2']; yield ['/f/news', '/f/NeWs/hot']; yield ['/f/news/new', '/f/NeWs/new']; - yield ['/f/news', '/f/NeWs/hot/1']; - yield ['/f/news/new', '/f/NeWs/new/1']; - yield ['/f/news/1', '/f/NeWs/1']; + yield ['/f/news/top', '/f/NeWs/top']; + yield ['/f/news/controversial', '/f/NeWs/controversial']; + yield ['/f/news/most_commented', '/f/NeWs/most_commented']; yield ['/f/news/1/comment/1', '/f/NeWs/1/comment/1']; + yield ['/f/news/hot.atom', '/f/news/hot/1.atom']; + yield ['/f/news/new.atom', '/f/news/new/1.atom']; } /** @@ -130,12 +139,14 @@ public function redirectUrlProvider() { public function authUrlProvider() { yield ['/subscribed/hot']; yield ['/subscribed/new']; - yield ['/subscribed/hot/1']; - yield ['/subscribed/new/1']; + yield ['/subscribed/top']; + yield ['/subscribed/controversial']; + yield ['/subscribed/most_commented']; yield ['/moderated/hot']; yield ['/moderated/new']; - yield ['/moderated/hot/1']; - yield ['/moderated/new/1']; + yield ['/moderated/top']; + yield ['/moderated/controversial']; + yield ['/moderated/most_commented']; yield ['/create_forum']; yield ['/f/news/edit']; yield ['/f/news/appearance']; diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 552675e..0d39e93 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -418,6 +418,7 @@ submissions: sort_by_new: New sort_by_top: Top sort_by_controversial: Controversial + sort_by_most_commented: Most commented total_votes: '{1} %count% point|[0,Inf[ %count% points' vote_stats: (+%up%, −%down%) edit_info: (edited %edited_at%) From 8568c41a7eaa20147c8209b72d2bd4a0c76141a7 Mon Sep 17 00:00:00 2001 From: Emma Date: Wed, 11 Apr 2018 01:35:43 +0200 Subject: [PATCH 18/24] fix typo that broke 'controversial' --- src/Repository/SubmissionRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Repository/SubmissionRepository.php b/src/Repository/SubmissionRepository.php index e52ded7..91a8591 100644 --- a/src/Repository/SubmissionRepository.php +++ b/src/Repository/SubmissionRepository.php @@ -51,7 +51,7 @@ class SubmissionRepository extends ServiceEntityRepository { private const CONTROVERSIAL_JOIN = '('. 'SELECT submission_id, COUNT(*) AS downvotes '. 'FROM submission_votes '. - 'WHERE NOT upvote = FALSE '. + 'WHERE upvote = FALSE '. 'GROUP BY submission_id'. ')'; From 0a1e653813112392f75298b66edcb1a41e003866 Mon Sep 17 00:00:00 2001 From: Emma Date: Wed, 11 Apr 2018 12:32:17 +0200 Subject: [PATCH 19/24] rearrange some classes, remove dead code --- config/services.yaml | 18 +++---- .../CachedMarkdownConverter.php | 2 +- src/{Utils => CommonMark}/MarkdownContext.php | 2 +- .../MarkdownConverter.php | 3 +- src/Controller/AjaxController.php | 4 +- src/Controller/UserController.php | 2 +- .../AuthenticationHelper.php | 2 +- src/Twig/FormattingExtension.php | 6 +-- src/Utils/PrependOrderBy.php | 51 ------------------- tests/Utils/CachedMarkdownConverterTest.php | 8 +-- tests/Utils/MarkdownContextTest.php | 2 +- tests/Utils/MarkdownConverterTest.php | 2 +- 12 files changed, 25 insertions(+), 77 deletions(-) rename src/{Utils => CommonMark}/CachedMarkdownConverter.php (97%) rename src/{Utils => CommonMark}/MarkdownContext.php (98%) rename src/{Utils => CommonMark}/MarkdownConverter.php (97%) rename src/{Utils => Security}/AuthenticationHelper.php (98%) delete mode 100644 src/Utils/PrependOrderBy.php diff --git a/config/services.yaml b/config/services.yaml index 96601f0..642d318 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -38,6 +38,10 @@ services: arguments: $defaultLocale: "%env(APP_LOCALE)%" + App\CommonMark\CachedMarkdownConverter: + arguments: + $cacheItemPool: "@cache.markdown" + App\Controller\ForumController: arguments: $enableWebhooks: "%env(bool:APP_ENABLE_WEBHOOKS)%" @@ -77,6 +81,11 @@ services: $noReplyAddress: "%env(NO_REPLY_ADDRESS)%" $salt: "%env(APP_SECRET)%" + App\Security\AuthenticationHelper: + arguments: + $rememberMeServices: "@security.authentication.rememberme.services.simplehash.main" + $secret: "%env(APP_SECRET)%" + App\Security\Voter\TokenVoter: arguments: $forumCreationInterval: "%user_forum_creation_interval%" @@ -86,15 +95,6 @@ services: $siteName: "%env(SITE_NAME)%" $enableWebhooks: "%env(bool:APP_ENABLE_WEBHOOKS)%" - App\Utils\CachedMarkdownConverter: - arguments: - $cacheItemPool: "@cache.markdown" - - App\Utils\AuthenticationHelper: - arguments: - $rememberMeServices: "@security.authentication.rememberme.services.simplehash.main" - $secret: "%env(APP_SECRET)%" - ## Misc diff --git a/src/Utils/CachedMarkdownConverter.php b/src/CommonMark/CachedMarkdownConverter.php similarity index 97% rename from src/Utils/CachedMarkdownConverter.php rename to src/CommonMark/CachedMarkdownConverter.php index 0b48a2d..f67cea7 100644 --- a/src/Utils/CachedMarkdownConverter.php +++ b/src/CommonMark/CachedMarkdownConverter.php @@ -1,6 +1,6 @@ getQueryPart('orderBy'); - - $qb->orderBy($clause, $order); - - foreach ($orderBy as $clause) { - preg_match('/^(.*) (ASC|DESC)$/', $clause, $matches); - $qb->addOrderBy($matches[1], $matches[2]); - } - } - - private static function prependWithDqlBuilder(DQLQueryBuilder $qb, string $clause, string $order) { - $orderBy = $qb->getDQLPart('orderBy'); - $qb->orderBy($clause, $order); - - foreach ($orderBy as $clause) { - $qb->addOrderBy($clause); - } - } -} diff --git a/tests/Utils/CachedMarkdownConverterTest.php b/tests/Utils/CachedMarkdownConverterTest.php index 0587fed..d5ae841 100644 --- a/tests/Utils/CachedMarkdownConverterTest.php +++ b/tests/Utils/CachedMarkdownConverterTest.php @@ -2,8 +2,8 @@ namespace App\Tests\Utils; -use App\Utils\CachedMarkdownConverter; -use App\Utils\MarkdownConverter; +use App\CommonMark\CachedMarkdownConverter; +use App\CommonMark\MarkdownConverter; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; @@ -42,7 +42,7 @@ public function testLoadFromCache() { ->expects($this->never()) ->method('convertToHtml'); - /* @var \App\Utils\CachedMarkdownConverter $cachedConverter */ + /* @var \App\CommonMark\CachedMarkdownConverter $cachedConverter */ $cachedConverter = $this->getMockBuilder(CachedMarkdownConverter::class) ->setConstructorArgs([$cacheItemPool, $converter]) ->enableProxyingToOriginalMethods() @@ -96,7 +96,7 @@ public function testSaveToCache() { ) ->willReturn('html output'); - /* @var \App\Utils\CachedMarkdownConverter $cachedConverter */ + /* @var \App\CommonMark\CachedMarkdownConverter $cachedConverter */ $cachedConverter = $this->getMockBuilder(CachedMarkdownConverter::class) ->setConstructorArgs([$cacheItemPool, $converter]) ->enableProxyingToOriginalMethods() diff --git a/tests/Utils/MarkdownContextTest.php b/tests/Utils/MarkdownContextTest.php index 9bf63f6..ee388a2 100644 --- a/tests/Utils/MarkdownContextTest.php +++ b/tests/Utils/MarkdownContextTest.php @@ -2,8 +2,8 @@ namespace App\Tests\Utils; +use App\CommonMark\MarkdownContext; use App\Entity\User; -use App\Utils\MarkdownContext; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/Utils/MarkdownConverterTest.php b/tests/Utils/MarkdownConverterTest.php index aefb3b8..be0d07c 100644 --- a/tests/Utils/MarkdownConverterTest.php +++ b/tests/Utils/MarkdownConverterTest.php @@ -2,7 +2,7 @@ namespace App\Tests\Utils; -use App\Utils\MarkdownConverter; +use App\CommonMark\MarkdownConverter; use PHPUnit\Framework\TestCase; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; From 64caf4761537a05e419b8fff28b36defc006c140 Mon Sep 17 00:00:00 2001 From: Emma Date: Wed, 11 Apr 2018 16:07:02 +0200 Subject: [PATCH 20/24] fix validation in SubmissionPager --- src/Repository/Submission/SubmissionPager.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Repository/Submission/SubmissionPager.php b/src/Repository/Submission/SubmissionPager.php index 5071a78..529e4ee 100644 --- a/src/Repository/Submission/SubmissionPager.php +++ b/src/Repository/Submission/SubmissionPager.php @@ -28,7 +28,7 @@ public static function getParamsFromRequest(string $sortBy, Request $request): a $value = $request->query->get('next_'.$column); $type = SubmissionRepository::SORT_COLUMN_TYPES[$column]; - if ($value === null || !self::valueIsOfType($type, $value)) { + if (!\is_string($value) || !self::valueIsOfType($type, $value)) { // missing columns - no pagination return []; } @@ -93,11 +93,11 @@ private function columnNameToAccessor(string $columnName): string { return 'get'.str_replace('_', '', ucwords($columnName, '_')); } - private static function valueIsOfType(string $type, $value): bool { + private static function valueIsOfType(string $type, string $value): bool { switch ($type) { case 'integer': return ctype_digit($value) && \is_int(+$value) && - $value >= 0x80000000 && $value <= 0x7fffffff; + $value >= -0x80000000 && $value <= 0x7fffffff; case 'bigint': // if this causes problems on 32-bit systems, the site operators // deserved it. From 8a7f61ab1dc83559fff5e06cb7584d03472a1f5f Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 12 Apr 2018 00:56:16 +0200 Subject: [PATCH 21/24] remove deprecated css classes --- templates/submission/base.html.twig | 19 +++++++------------ templates/submission/submission.html.twig | 4 +--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/templates/submission/base.html.twig b/templates/submission/base.html.twig index 7ce799f..8066b9e 100644 --- a/templates/submission/base.html.twig +++ b/templates/submission/base.html.twig @@ -4,24 +4,19 @@ {{ submission.title }} {% endblock %} -{# deprecated classes: - - sidebar__section--submission-meta - - sidebar-submission-meta - - sidebar-submission-meta-score - - vote-total - - vote-stats -#} {% block sidebar %} -